《深入理解 Java 内存模型》读书笔记(下)(干货,万字长文)
-
0. 前提
-
1. 基礎
-
2. 重排序
-
3. 順序一致性
-
4. Volatile
-
5. 鎖
-
6. final
-
7. 總結
4. Volatile
4.1 VOLATILE 特性
舉個例子:
public?class?VolatileTest?{volatile?long?a?=?1L;?????????//?使用?volatile?聲明?64?位的?long?型public?void?set(long?l)?{a?=?l;??????????????????//單個?volatile?變量的寫}public?long?get()?{return?a;???????????????//單個?volatile?變量的讀}public?void?getAndIncreament()?{a++;????????????????????//?復合(多個)?volatile?變量的讀?/寫} }假設有多個線程分別調用上面程序的三個方法,這個程序在語義上和下面程序等價:
public?class?VolatileTest?{long?a?=?1L;?????????????????//?64?位的?long?型普通變量public?synchronized?void?set(long?l)?{????//對單個普通變量的寫用同一個鎖同步a?=?l;????????????????}public?synchronized?long?get()?{????????//對單個普通變量的讀用同一個鎖同步return?a;???????????}public?void?getAndIncreament()?{????????//普通方法調用long?temp?=?get();??????????????????//調用已同步的讀方法temp?+=?1L;????????????????????????????//普通寫操作?????????????????????????set(temp);??????????????????????????//調用已同步的寫方法} }如上面示例程序所示,對一個 volatile 變量的單個讀/寫操作,與對一個普通變量的讀/寫操作使用同一個鎖來同步,它們之間的執行效果相同。
鎖的 happens-before 規則保證釋放鎖和獲取鎖的兩個線程之間的內存可見性,這意味著對一個 volatile 變量的讀,總是能看到(任意線程)對這個 volatile 變量最后的寫入。
鎖的語義決定了臨界區代碼的執行具有原子性。這意味著即使是 64 位的 long 型和 double 型變量,只要它是 volatile變量,對該變量的讀寫就將具有原子性。如果是多個 volatile 操作或類似于 volatile++ 這種復合操作,這些操作整體上不具有原子性。
簡而言之,volatile 變量自身具有下列特性:
-
可見性。對一個 volatile 變量的讀,總是能看到(任意線程)對這個 volatile 變量最后的寫入。
-
原子性:對任意單個 volatile 變量的讀/寫具有原子性,但類似于 volatile++ 這種復合操作不具有原子性。
4.2 VOLATILE 寫-讀的內存定義
-
當寫一個 volatile 變量時,JMM 會把該線程對應的本地內存中的共享變量值刷新到主內存。
-
當讀一個 volatile 變量時,JMM 會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
假設上面的程序 flag 變量用 volatile 修飾
volatile14.3 VOLATILE 內存語義的實現
下面是 JMM 針對編譯器制定的 volatile 重排序規則表:
重排序規則表為了實現 volatile 的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
下面是基于保守策略的 JMM 內存屏障插入策略:
-
在每個 volatile 寫操作的前面插入一個 StoreStore 屏障。
-
在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障。
-
在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障。
-
在每個 volatile 讀操作的后面插入一個 LoadStore 屏障。
下面是保守策略下,volatile 寫操作 插入內存屏障后生成的指令序列示意圖:
volatile3下面是在保守策略下,volatile 讀操作 插入內存屏障后生成的指令序列示意圖:
volatile4上述 volatile 寫操作和 volatile 讀操作的內存屏障插入策略非常保守。在實際執行時,只要不改變 volatile 寫-讀的內存語義,編譯器可以根據具體情況省略不必要的屏障。
5.1 鎖
5.2 鎖釋放和獲取的內存語義
當線程釋放鎖時,JMM 會把該線程對應的本地內存中的共享變量刷新到主內存中。
當線程獲取鎖時,JMM 會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須要從主內存中去讀取共享變量。
5.3 鎖內存語義的實現
借助 ReentrantLock 來講解,PS: 后面專門講下這塊(ReentrantLock、Synchronized、公平鎖、非公平鎖、AQS等),可以看看大明哥的博客:http://cmsblogs.com/?p=2210
5.4 CONCURRENT 包的實現
如果我們仔細分析 concurrent 包的源代碼實現,會發現一個通用化的實現模式:
首先,聲明共享變量為 volatile;
然后,使用 CAS 的原子條件更新來實現線程之間的同步;
同時,配合以 volatile 的讀/寫和 CAS 所具有的 volatile 讀和寫的內存語義來實現線程之間的通信。
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic 包中的類),這些 concurrent 包中的基礎類都是使用這種模式來實現的,而 concurrent 包中的高層類又是依賴于這些基礎類來實現的。從整體來看,concurrent 包的實現示意圖如下:
concurrent 包6. final
對于 final 域,編譯器和處理器要遵守兩個重排序規則:
在構造函數內對一個 final 域的寫入,與隨后把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
初次讀一個包含 final 域的對象的引用,與隨后初次讀這個 final 域,這兩個操作之間不能重排序。
6.1 寫 FINAL 域的重排序規則
寫 final 域的重排序規則禁止把 final 域的寫重排序到構造函數之外。這個規則的實現包含下面2個方面:
-
JMM 禁止編譯器把 final 域的寫重排序到構造函數之外。
-
編譯器會在 final 域的寫之后,構造函數 return 之前,插入一個 StoreStore 屏障。這個屏障禁止處理器把 final 域的寫重排序到構造函數之外。
6.2 讀 FINAL 域的重排序規則
在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器)。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。
6.3 FINAL 域是引用類型
對于引用類型,寫 final 域的重排序規則對編譯器和處理器增加了如下約束:
在構造函數內對一個 final 引用的對象的成員域的寫入,與隨后在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
7. 總結
7.1 JMM,處理器內存模型與順序一致性內存模型之間的關系
JMM 是一個語言級的內存模型,處理器內存模型是硬件級的內存模型,順序一致性內存模型是一個理論參考模型。下面是語言內存模型,處理器內存模型和順序一致性內存模型的強弱對比示意圖:
內存模型比較7.2 JMM 的設計示意圖
jmm7.3 JMM 的內存可見性保證
Java 程序的內存可見性保證按程序類型可以分為下列三類:
1.單線程程序。單線程程序不會出現內存可見性問題。編譯器,runtime 和處理器會共同確保單線程程序的執行結果與該程序在順序一致性模型中的執行結果相同。
2.正確同步的多線程程序。正確同步的多線程程序的執行將具有順序一致性(程序的執行結果與該程序在順序一致性內存模型中的執行結果相同)。這是 JMM 關注的重點,JMM通過限制編譯器和處理器的重排序來為程序員提供內存可見性保證。
3.未同步/未正確同步的多線程程序。JMM 為它們提供了最小安全性保障:線程執行時讀取到的值,要么是之前某個線程寫入的值,要么是默認值(0,null,false)。
下圖展示了這三類程序在 JMM 中與在順序一致性內存模型中的執行結果的異同:
總結
以上是生活随笔為你收集整理的《深入理解 Java 内存模型》读书笔记(下)(干货,万字长文)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《深入理解 Java 内存模型》读书笔记
- 下一篇: 北上广Java开发月薪20K往上,该如何