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