十年程序員淺談併發的原子性、可見性、有序性

程序員 Java Java虛擬機 中央處理器 編譯器 虛擬機 程序猿的內心獨白 2019-04-26

內存模型與運行時數據區

內存模型

java內存模型簡稱JMM(Java Memory Model ),定義了程序中各個共享變量的訪問規則。

十年程序員淺談併發的原子性、可見性、有序性

Java Memory Model

變量存儲在主內存中,每個線程擁有自己的工作內存用來存放變量的拷貝,線程的讀寫操作是在各自的工作內存中進行的,操作的對象都是變量的拷貝,操作完畢後在刷新到主內存。

JMM規範定義了工作內存和主內存之間變量訪問的細節,通過保障原子性、有序性、可見性實現線程安全。

運行時數據區

運行時數據區(JVM Runtime Data Areas)定義了JVM運行期內存的管理劃分。

十年程序員淺談併發的原子性、可見性、有序性

JVM Runtime Data Areas

JVM在運行時把內存劃分成多個功能區,每個區域對應著不能的存儲內容,生命週期,共享性質,GC策略等。

可以看到,能被線程共享的是方法區和堆中的數據,也就是實例對象、數組和靜態變量,這些共享數據受到JMM規範影響。

而局部變量、方法參數、異常處理參數都在虛擬機棧中,這些數據為線程私有的,所以不受JMM規範影響。

原子性、可見性、有序性

原子性

原子操作是指一個操作不會被線程調度機制打斷,一旦開始,就一直運行到結束,中間不會有任何線程切換(context switch)。

原子性可以保障讀取到的某個屬性的值是由一個線程寫入的。 變量不會在同一時刻受到多個線程同時寫入造成干擾。如在32位的JVM中對64位long 或double值的寫操作是分成兩次相鄰的32位值寫操作,在多線程的環境下,可能會有線程只讀到了前32位,這種操作就是非原子性的,非原子性操作會受到多線程的干擾而產生結果混亂。

基本類型的單次讀寫操作是原子的,但是複合操作如:int i=0;i++,就是非原子性的

JMM保障原子性的方法:volatile語義(保證變量單次操作的的原子性)、鎖語義

可見性

十年程序員淺談併發的原子性、可見性、有序性

共享內存模型

可見性是指一個線程對變量的值進行了修改,其他線程能夠立即得知這個修改。

如上圖:在共享內存模型中如果有一個線程對變量i進行了修改,在沒有可見性保障的情況下,其他兩個線程看到的i的值都是不確定的,變量i在數據爭用的情況下不具備不可見性。

可見性是保障多線程操作中數據一致性和結果正確性的基石,多線程環境下影響變量可見性的因素:

1、 指令重排序

2、 線程調度(切換)

3、 工作內存和主內存沒有及時刷新

JMM保障可見性的方法:fianl語義、volatile語義、鎖語義

有序性

現代CPU的計算速度遠遠高於內存的讀寫速度,CPU會採用高速緩存來抵消內存訪問帶來的延遲。甚至高速緩存也分成多級,最快的離CPU最近,但是其存取速度還是遠遠低於CUP指令執行的速度,為了減少CACHE_WAIT,CPU會採用指令級並行重排序來提供執行效率,也可以叫做CPU亂序執行。

CUP的高速緩存與內存之間不是實時同步的,高速緩與高速緩間也不是實時同步,而是通過緩存一致性協議(MESI)將數據新到主內存,緩存和讀寫緩衝區之間也會通過指令重排序來優化數據的刷新。

JIT編譯器也會在代碼編譯的時候對代碼進行重新整理,最大限度的去優化代碼的執行效率。

所以一段JAVA代碼從執行到獲得結果,其執行的順序其實是經歷了2個階段三種重排序的優化:

十年程序員淺談併發的原子性、可見性、有序性

代碼重排序過程

保障重排序後結果正確性

1、as-if-serial語義

as-if-serial語義的意思指:所有的指令都可以為了優化而被重排序,但是必須保證最終執行的結果和重排序之前的結果是一致的,編譯器和處理器都會保證單線程下的as-if-serial語義。主要遵守的規則是重排序不破壞數據的依賴關係,如下圖,指令C依賴指令A和指令B,那麼重排序只能在指令A和指令B之間發生。

十年程序員淺談併發的原子性、可見性、有序性

數據依賴關係

as-if-serial語義保證了單線程環境下重排序之後程序執行結果的正確性,JVM在單線程的情況下會遵as-if-serial語義,無需擔心重排序會干擾心內存可見性。

2、happens-before原則

十年程序員淺談併發的原子性、可見性、有序性

示例1

按照寫代碼的主觀意願,可能期望是要麼指令1先執行,要麼指令3先執行,指令1先執行就不應該看到到指令4寫入的值,如果是指令3先執行,就不應該看到指令2寫入的值。

如果編譯器或者執行CPU進行了重排序,指令4在指令1前先執行了,指令2在指令3之前執行了,就會出現r2 == 2和r1 == 1這種有違直覺的結果。然而,從單個線程的角度,指令1和指令2重排序是遵循as-if-serial語義的,不會影響該線程獲得正確的結果。但是,從多線程的角度看,編譯器或者指令重排序影響到了代碼原本想要表達語義。

十年程序員淺談併發的原子性、可見性、有序性

示例2

這個示例中指令1和指令2之間沒有依賴關係遵循as-if-serial語義重排序,對單線程執行結果的正確性沒有影響,但是多線程環境下,如果thread1執行完指令1,thread2執行,那i的值會出現有背預期的情況,因為thread1中對共享變量a的修改,對thread2是不可見的。

基於數據依賴性的as-if-serial語義無法保證多線程環境下,重排序之後程序執行結果的正確性。JMM中happens-before原則就是用來保障多線程環境下變量可見性的。

先行發生原則( happens-before )是JMM用來規定兩個操作之間的偏序關係,這兩個操作是可以跨線程的。happens-before中確定了8條規則,如果如果兩個操作之間的關係可以從下列規則推導出來說明兩個操作是有序的。

happens-before並不限定指令重排序,如果如果重排序之後的執行結果與按happens-before關係來執行的結果一致,那麼JVM允許這種重排序。happens-before原則保證了前後兩個操作間不會被重排序且後者對前者的內存是可見的。

happens-before八條規則:

1、程序次序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作(一個線程內保證語義的串行性)。

2、鎖定規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。

3、volatile變量規則:volatile變量的寫操作happens-before於後面對這個變量的讀操作。

4、傳遞規則:如果A happens-before B且Bhappens-before C,那麼A happens-before C。

5、線程啟動規則:Thread對象的start()方法happens-before於此線程的每個一動作。

6、線程中斷規則:對線程interrupt()方法的調用happens-before於被中斷線程的代碼檢測到中斷事件的發生。

7、線程終結規則:線程中所有的操作都happens-before於線程的終止。

8、對象終結規則:一個對象的初始化完成happens-before於他的finalize()方法的開始。

小結

1、JMM規範定義了工作內存和主內存之間變量訪問的細節,通過保障原子性、有序性、可見性實現線程安全。

2、線程調度(切換)會影響數據操作的原子性,JMM通過fianl語義、volatile語義、鎖語義來保障原子性。

3、線程調度(切換)、指令重排序、內存刷新都會影響可見性,JMM通過volatile語義、鎖語義來保障可見性。

4、內存系統重排序、指令級並行重排序、編譯器優化重排序都會影響到程序執行的有序性,JMM通過happens-before原則保障併發環境下程序執行的有序性。

相關推薦

推薦中...