多線程三大特性:原子性、可見性、有序性。java
原子性是指:多個操做做爲一個總體,不能被分割與中斷,也不能被其餘線程干擾。若是被中斷與干擾,則會出現數據異常、邏輯異常。編程
多個操做合併的總體,咱們稱之爲複合操做。一個複合操做,每每存在先後依賴關係,後一個操做依賴上一個操做的結果。若是上一個操做結果被其餘線程干擾,對於當前線程看來整個複合操做的結果便不符合預期。同理線程也不能在複合操做中間被中斷,中斷必須發生在進入複合操做以前或者等到複合操做結束以後。數組
保證原子性就是在多線程環境下,保證單個線程執行復合操做符合預期邏輯。緩存
典型的複合操做:『先檢查後執行』和『讀取—修改—寫入』多線程
@NotThreadSafe public class LazyInitClass { private static LazyInitClass instance ; public static LazyInitClass getInstance() { if(instance == null) instance = new LazyInitClass() ; return instance ; } }
LazyInitClass
的 getInstance
中包含先檢查後執行的複合操做,一般咱們也能夠稱 getInstance
中包含競態條件。假設線程 A 和線程 B 同時執行 getInstance
。A 看到 instance
爲空,便執行 new LazyInitClass()
邏輯。A 還未完成初始化並設置 instance
,B 檢查 instance
,此時 instance
爲空,B 便也會執行 new LazyInitClass()
。那麼兩次調用 getInstance
時可能會獲得不一樣的結果。一般 getInstance
的預期結果是屢次調用獲得相同的對象實例。性能
LazyInitClass
的 getInstance
方法雖然存在競態條件,多數狀況下並不會形成業務異常,影響僅僅是增長了 JVM 垃圾回收負擔而已。這也是多線程問題隱蔽性強且偶發的緣由之一。編碼
但話說回來,編程原則之一就是全部邏輯都必須創建在肯定性之上,任何創建在不肯定性上的邏輯都是隱患。雖然從業務上看多數狀況下沒問題,但競態條件的存在,讓代碼邏輯創建在不肯定性之上。做爲編碼者應該重視此類問題。spa
@NotThreadSafe public class ReadModifyAndWriteClass { private int count = 0 ; public int increase() { return count++ ; } }
因爲 i++ 自己不是原子操做,屬於複合操做。ReadModifyAndWriteClass
的 increase
包含了讀取—修改—寫入。假設線程 A 和線程 B 同時執行 increase
。A 看到 count 爲 0,執行 ++ 邏輯。當 ++ 操做還未完成,此時 B 讀取 count 看到的仍然是 0。A、B 各自完成 ++ 邏輯後,count 的值等於 1。這就形成了雖然調用了兩次 increase
方法,但 count 只增長了 1。這也與預期:每調用一次 increase
,count 增長 1 的結果不符。線程
可見性問題是指,一個線程修改的共享變量,其餘線程是否可以馬上看到。對於串行程序而言,並不存在可見性問題,前一個操做修改的變量,後一個操做必定能讀取到最新值。但在多線程環境下若是沒有正確的同步則不必定。設計
有不少因素會使得線程沒法當即看到甚至永遠沒法看到另外一個線程的操做結果。在編譯器中生成的指令順序,能夠與源代碼中的順序不一樣,此外編譯器還會把變量保存在寄存器而非內存中;處理器能夠採用亂序或並行等方式來執行指令;緩存可能會改變將寫入變量提交到主內存的次序;並且,保存在處理器本地緩存中的值,對於其餘處理器是不可見的。這些因素都會使得一個線程沒法看到變量的最新值,而且會致使其餘線程中的內存操做彷佛在亂序執行。
上圖是多核 CPU 內存圖,其中 individual memory 表示核心多級緩存。main memory 表示主內存,即共享內存。共享內存(shared memory)是線程之間共享的內存,也稱爲堆內存(heap memory)。全部實例域(instance fields)、靜態域(static fields)和數組元素(array elements)都保存在堆內存中。
A 線程與 B 線程共同操做共享變量 V(初始值爲 0),A、B 線程分別將 V 變量從主內存複製到 CPU 內核的多級緩存中,此時 A 與 B 都讀到 V 的值爲 0。A 更新本身的 individual memory 中的 V 的值爲 1,此時若是沒有將 V 值同步至主內存中,B 從本身的 individual memory 中讀到 V 的值仍然爲 0。當 V 值同步到主內存後,多級緩存失效,此時 B 纔可以從主內存中讀取到最新的 V 值爲 1。因爲多線程環境下什麼時候將多級緩存同步到主內存時間上不肯定,因此形成了可見性問題,即 A 線程對共享變量 V 的寫操做,位於寫操做後執行的 B 線程的讀操做不能當即感知。
有序性問題是指從觀察到的結果推測,代碼執行的順序與代碼組織的順序不一致。
在計算機體系結構中,爲了提升執行部件的處理速度,常常在部件中採用流水線技術。所謂流水線技術,是指將一個重複的時序過程,分解成若干個子過程,而每個子過程均可有效地在其專用功能段上與其餘子過程同時執行。
以 DLX 指令集結構爲例,一條指令的執行簡單說能夠分爲如下幾個步驟:
每個步驟均可能使用不一樣的硬件完成。
由上圖所示,若是沒有指令流水線,指令2 須要等待指令1 徹底執行完成後執行。假設每個步驟(子過程)須要花費 1 個 CPU 時鐘週期,則指令2 須要等待 5 個時鐘週期。而使用指令流水線後,指令2 只需等待 1 個時鐘週期就能夠開始執行。指令2 開始執行時,指令1 根本還沒開始執行,僅僅完成了取指操做而已。這僅僅是 DLX 指令集結構的流水線,實際商用 CPU 的流水線級別甚至能夠達到 10 級以上,性能提高可謂是很是明顯。
因爲流水線技術的引入,不得不面對流水線的三種類型的相關:結構相關、數據相關、控制相關。
一旦流水線中出現相關,指令在流失線中的執行就會出現問題,消除相關的最基本方法是讓流水線中的某些指令暫停執行。一旦暫停,全部硬件設備都會進入一個停頓週期,直接影響是性能的降低。
咱們說的指令重排序就是在產生數據相關時替代流水線暫停的重要方法。指令重排序僅僅是減小流水線暫停技術的一種,在 CPU 設計中還有不少其餘軟硬件技術來防止流水線暫停。
下圖展現了 A = B + C 操做的執行過程。LW 表示加載,LW R1, B 表示把 B 的值加載到寄存器 R1 中。ADD 表示加法,ADD R3, R1, R2 表示把寄存器 R1 和 R2 中的值相加保存到寄存器 R3 中。SW 表示存儲,SW A, R3 表示將寄存器 R3 中的值保存到變量 A 中。
能夠看到,ADD 指令的流水線上出現了一個 stall,表示一個暫停。之因此出現暫停,是由於 R2 的數據還沒準備好( LW R2, C 的操做還沒完成 )。因爲 ADD 暫停的出現,後續的操做都暫停了一個週期。
下面是一個更爲複雜的例子:
能夠看到,因爲 ADD 和 SUB 指令都須要等待上一條指令的執行結果,因此整個流水線上插入了很多 stall。下圖顯示瞭如何消除相似的暫停。
因爲 LW Re, E; LW Rf, F 通過指令重排序後,並不影響代碼執行邏輯。而且當重排序後,全部流水線暫停均可以消除。
雖然指令重排序會致使有序性問題,但指令重排序對性能的提升有很是重大的意義。
2.1 節已經討論過 CPU 緩存致使的可見性問題。CPU 緩存也會致使有序性問題。
看以下的例子:
假設 b、c 爲局部變量,初始值爲 1,A、D 爲共享變量,初始值爲 0 和 false。Thread1 先於 Thread2 運行,運行結果:Thread2 輸出 0。
從結果推測 Thread1 中的 D = true 先於 A = b + c 執行了。
當 D = true 執行完成後,A = b + c 還沒來得及執行,此時 Thread2 輸出 A 的值,纔會出現結果爲 0 的狀況。
分析:Thread1 將 A、D 共享變量從主內存複製到當前 CPU 內核的多級緩存中,按順序執行完 A = b + c 和 D = true 後,多級緩存中 A = 2, D = true。而後 Thread1 將 D 的值優先同步到主緩存,A 的值沒有同步到主緩存。此時 Thread2 執行,能看到 D 的最新值 true,卻不能看到 A 的最新值,只能看到主緩存中 A 的初始值 0。
因此從 Thread2 看,Thread1 線程的執行出現了有序性問題,但從 Thread1 看,本身的確是按照代碼組織順序執行的。
本章詳細講解了多線程的三大特性:原子性、可見性、有序性。想要正確編寫多線程程序,必定要正確理解這三大特性。