首頁>Club>
4
回覆列表
  • 1 # 此生唯一

    執行緒安全有三大特性:原子性,可見性,有序性,只有三大特性都滿足的時候才能保證執行緒安全,三大特性詳細描述如下:

    1,原子性:通常是指程式碼執行的效果,要麼全部執行成功,要麼全部失敗;

    2,可見性:執行緒中的本地記憶體中的變數值,應該立即同步到主記憶體中,讓其他執行緒可見;

    3,有序性:保證程式碼執行的序列性;

    java中的原子性操作主要是使用sun.misc.Unsafe包下的compareAndSwap方法實現,這是sun包下native方法,使用c++程式碼實現,在CPU層級來 保證底層指令的原子性,或者使用加鎖的方式!

    可見性無法保證主要是因為,記憶體的存取速度跟CPU相比存在差距,所以在CPU和主記憶體之間,引入了快取(執行緒本地記憶體)的概念,來增加CPU 的計算效能,副作用就是導致執行緒操作共享資料時,無法保證中間資料是最新的(有可能還在別的執行緒的本地記憶體彙總)!

    有序性無法保證是因為基於效能的考慮,編譯器和處理器會對操作指令進行重排序,在單執行緒之中不存在問題,但如果是多執行緒就可能存在 判斷錯誤的情況!

    編譯器的重排序也是規則的,java中先天的有序性組成了happens-before原則,如果happens-before原則不能推匯出指令的執行次序,則指令 就不是有序的,happens-before八大原則為:程式次序規則(單執行緒中的順序性,多執行緒無法保證),鎖定規則(同一個鎖先解鎖,後加鎖) ,volatile變數規則(先寫後讀),傳遞規則(類似A早於B,B早於C,則A早於C),執行緒啟動原則(start方法是執行緒最先執行的方法),執行緒 中斷規則,執行緒終結規則,物件終結規則!happens-before八大原則規定了指令執行的有序性

    由此可以看出,執行緒不安全的原因十有八九都是追求效能惹的禍!

    通常程式碼在滿足三大特性的時候,就能保證執行緒安全,java中保證執行緒安全的方式有很多,包括加鎖和不加鎖,下面來逐一談下:

    1,加鎖:比如synchronized(JDK中自帶的關鍵字,JMM規定獲取鎖的時候,必須清空工作記憶體中的變數值,保證需要 獲取變數的時候,只能從主記憶體中獲取;釋放鎖的時候,必須把最新值寫入到主記憶體中,這樣來保證資料的可見性和原子性)!

    還有基於AQS實現的reentrantLock,ReentrantReadWriteLock等都是加鎖!

    2,不加鎖:

    ①,使用volatile+CAS操作,volatile使用記憶體屏障來保證變數的可見性和指令有序性,CAS保證原子性,最終實現執行緒安全,在jdk中Atomic 打頭的幾個類,都是用了這種方式,實現執行緒安全!如下圖:

    ②,使用ThreadLocal,每個執行緒都維護一份資料到自己的本地記憶體中,相當於沒有共享資源的競爭,所以也不會有執行緒安全問題;

  • 2 # 架構思維

    首先需要了解,為什麼會有「可見性」和「時序性」問題,然後我們來看Java是如何解決這兩個問題的。

    「可見性」和「時序性」問題

    導致「可見性」和「時序性」問題的原因有如下幾個:

    搶佔式任務執行:現代CPU執行多工方式是「搶佔式」,它的總控制權在作業系統手中,作業系統會輪流給需要CPU執行的任務分配執行時間片,超過時間後,作業系統會剝奪當前任務的 CPU 使用權,把它排在佇列的最後,最後分配時間片……

    儲存速度差異:各儲存執行速度的不同,離CPU越近,儲存速度越快,相對的容量就越小。執行程式所需要的資料不可能一次性全部都載入到暫存器中,所以有load與store的過程,影響了所謂的「可見性」

    指令重排:大多數現代微處理器都會採用將指令亂序執行(out-of-order execution,簡稱OoOE或OOE)的方法,在條件允許的情況下,直接運行當前有能力立即執行的後續指令,避開獲取下一條指令所需資料時造成的等待。透過亂序執行的技術,處理器可以大大提高執行效率。除了處理器,常見的Java執行時環境的JIT編譯器也會做指令重排序操作,即生成的機器指令與位元組碼指令順序不一致。

    解決方法

    解決思路很簡單,就是把多執行緒強制單執行緒執行。

    解決方法無非兩種:

    記憶體屏障

    先看下JVM的記憶體模型,我們基於這個模型來簡單說明下

    記憶體屏障

    記憶體屏障在Java中透過volatile關鍵字型現。volatile會在適當的地方新增下面四種記憶體屏障。

    LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。

    StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。

    LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。

    StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能。

    記憶體屏障只保證可見性,不保證時序性。也就是說記憶體屏障只是解決了執行緒A修改的內容能立刻被執行緒B讀到。

    Java中鎖按性質分可以分悲觀鎖和樂觀鎖。悲觀鎖基於鎖指令實現,樂觀鎖基於CAS實現。

    透過monitorenter和monitorexit兩個指令實現悲觀鎖,這兩個指令之間的指令不得重排,且獨佔。假設執行緒A和執行緒B同時執行一段程式碼,執行緒A先透過monitorenter獲取到了鎖,那麼線上程A執行monitorexit之前,執行緒B都只能等待。

    CAS即CompareAndSet,Java透過自旋以及CPU層級的指令實現。具體可參考JUC實現。假設有一個變數c,初始值為3。執行緒A和執行緒B同時修改這個變數,A,B都同時獲取到了變數c的值,A首先進行修改,將值改成了4。B嘗試修改,但是發現c的值現在是4而不是3,所以進行自旋等待,然後重新執行修改操作,將4改成了5。

    ThreadLocal

    最後說下ThreadLocal。ThreadLocal即本地執行緒變數,也就是將公共的變數直接拿到執行緒內使用,其中的修改對外不影響。談不上解決了「可見性」和「時序性」。只是保證了當前執行緒內的修改不影響其它執行緒,其它執行緒的修改也不影響當前執行緒。

  • 中秋節和大豐收的關聯?
  • 如何看待有些人說庫裡是常規賽球員這種言論?