Java并發(fā)編程的藝術(shù)筆記
- 1.并發(fā)編程的挑戰(zhàn)
- 2.Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理
- 3.Java內(nèi)存模型
- 4.Java并發(fā)編程基礎(chǔ)
- 5.Java中的鎖的使用和實(shí)現(xiàn)介紹
- 6.Java并發(fā)容器和框架
- 7.Java中的12個(gè)原子操作類介紹
- 8.Java中的并發(fā)工具類
- 9.Java中的線程池
- 10.Executor框架
目錄
內(nèi)存模型基礎(chǔ)volatile的內(nèi)存語義鎖的內(nèi)存語義final域的內(nèi)存語義happens-before雙重檢查鎖定與延遲初始化Java內(nèi)存模型綜述小結(jié)
內(nèi)存模型基礎(chǔ)
1、并發(fā)編程的兩個(gè)關(guān)鍵問題
-
線程之間如何通信?
通信是指 以何種機(jī)制來交換信息。
命令式編程中線程的通信機(jī)制主要是以下兩種:- 共享內(nèi)存 的并發(fā)模型:通過 讀寫內(nèi)存中的公共狀態(tài) 來進(jìn)行隱式通信。
- 消息傳遞 的并發(fā)模型:沒有公共狀態(tài),只能 通過發(fā)送消息來顯示的進(jìn)行通信。
-
線程之間如何同步?
同步是指 程序中用于控制不同線程間操作發(fā)生相對(duì)順序 的機(jī)制。- 共享內(nèi)存 的并發(fā)模型:同步時(shí)顯示進(jìn)行的。我們必須顯示指定某段代碼需要在線程直線互斥執(zhí)行。
- 消息傳遞 的并發(fā)模型:由于消息發(fā)送必須在消息接收之前,因此同步時(shí)隱式的。
Java并發(fā) 采用的是 共享內(nèi)存模型,Java線程之前的通信總是隱式進(jìn)行的。
2、Java內(nèi)存模型的抽象結(jié)構(gòu)
在Java中,所有 實(shí)例域、靜態(tài)域 和 數(shù)組元素 都儲(chǔ)存在堆內(nèi)存中,堆內(nèi)存在線程之前共享。
本文用 共享變量 統(tǒng)一描述 實(shí)例域、靜態(tài)域 和 數(shù)組元素 。
局部變量 、方法定義參數(shù)、異常處理器參數(shù) 不會(huì)在內(nèi)存之間共享,他們不會(huì)有內(nèi)存可見性問題,也不受內(nèi)存模型影響。
Java線程通信由Java內(nèi)存模型(簡(jiǎn)稱 JMM)控制,JMM 決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見。
從抽象角度看,JMM定義了 線程 和 主內(nèi)存 之間的抽象關(guān)系:線程之間的共享變量?jī)?chǔ)存在主內(nèi)存中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存,本地內(nèi)存儲(chǔ)存了 該線程 以讀寫共享變量的副本。
從上圖來看,線程A和線程B需要通信的話,需要經(jīng)歷以下步驟:
1、線程A 把 本地內(nèi)存A 中的 共享變量副本 刷新到 主內(nèi)存 中。
2、線程B 去讀取 主內(nèi)存 中 線程A 刷新過的 共享變量。
從整體來看,這兩個(gè)步驟實(shí)質(zhì)上是線程A向線程B發(fā)送消息,而通信必須經(jīng)過主內(nèi)存。
JMM 通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來提供內(nèi)存可見性的保證。
3、從源代碼到指令序列的重排序
執(zhí)行程序的時(shí)候,為了提高性能,編譯器 和 處理器 常常會(huì)對(duì)指令做 重排序。
主要有以下三類:
- 編譯器優(yōu)化的重排序 :編譯器在 不改變單線程程序語義 的前提下,可以重新安排語句的執(zhí)行順序。
- 指令級(jí)并行的重排序 : 現(xiàn)代處理器采用 并行技術(shù) 來將多條指令重疊執(zhí)行,如果不存在數(shù)據(jù)依賴性,處理器可以改變對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序 : 由于處理使用緩存和讀寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能亂序執(zhí)行。
以下描述了源代碼到最終執(zhí)行的指令序列的示意圖:
上圖中的 1 屬于 編譯器重排序,2 和 3 屬于 處理器重排序。這些重排序可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題。
對(duì)于編譯器重排序, JMM的編譯器重排序規(guī)則 會(huì)禁止特定類型的編譯器重排序。
對(duì)于處理器重排序,JMM的處理器重排序規(guī)則 會(huì)要求編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障指令,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序。
4、happens-before簡(jiǎn)介
從 JDK5 開始,Java使用新的 JSR-133 內(nèi)存模型。 JSR-133 使用 happens-before 的概念來闡述操作之間的內(nèi)存可見性。在 JMM 中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見,則這兩個(gè)操作必須要存在happen-before關(guān)系 。
happen-before 規(guī)則如下:
- 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happen-before與該線程中的任意后續(xù)操作
- 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happen-before與隨后這個(gè)鎖的加鎖
- volatile變量規(guī)則:對(duì)于一個(gè)volatile域的寫,happen-before與任意后續(xù)對(duì)這個(gè)volatile域的讀
- 傳遞性: A happen-before B,B happen-before C,則A happen-before C
volatile的內(nèi)存語義
理解volatile特性的一個(gè)好方法是把對(duì)volatile變量的單個(gè)讀/寫,看成是 使用同一個(gè)鎖 對(duì)這些單個(gè)讀/寫操作做了同步。
volatile變量具有下列特性:
- 可見性:總是能看到(任意線程)對(duì)這個(gè)
volatile變量最后的寫入。 - 原子性:對(duì)任意單個(gè)
volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性。
volatile寫的內(nèi)存語義:當(dāng)寫一個(gè)volatile變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
volatile讀的內(nèi)存語義:當(dāng)讀一個(gè)volatile變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。
volatile內(nèi)存語義的實(shí)現(xiàn):
為了實(shí)現(xiàn)volatile內(nèi)存語義,JMM 會(huì)分別限制這兩種類型的重排序類型。
以下是 JMM 針對(duì) 編譯器 制定的 volatile重排序規(guī)則表:
| 是否能重排序 | 第二個(gè)操作 | 第二個(gè)操作 | 第二個(gè)操作 |
|---|---|---|---|
| 第一個(gè)操作 | 普通讀寫 | volatile讀 | volatile寫 |
| 普通讀寫 |
(1)NO |
||
| volatile讀 | NO | NO | NO |
| volatile寫 | NO | NO |
第三行最后一個(gè)單元格(1)的意思是:在程序中,當(dāng)?shù)谝粋€(gè)操作為普通變量的讀或?qū)憰r(shí),如果第二個(gè)操作為volatile寫,則編譯器不能重排序這兩個(gè)操作。
在上表中,我們可以知道:
- 當(dāng)?shù)诙€(gè)操作是
volatile 寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile 寫之前的操作不會(huì)被編譯器重排序到volatile 寫之后。 - 當(dāng)?shù)谝粋€(gè)操作是
volatile 讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile 讀之后的操作不會(huì)被編譯器重排序到volatile 讀之前。 - 當(dāng)?shù)谝粋€(gè)操作是
volatile 寫,第二個(gè)操作是volatile 讀時(shí),不能重排序。
為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入 內(nèi)存屏障 來禁止特定類型的 處理器重排序。
對(duì)于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能。
為此,JMM采取保守策略。
下面是基于保守策略的JMM內(nèi)存屏障插入策略。
- 在每個(gè)
volatile 寫操作的前面插入一個(gè)StoreStore屏障,后面插入一個(gè)StoreLoad屏障。 - 在每個(gè)
volatile 讀操作的后面插入一個(gè)LoadLoad屏障,后面插入一個(gè)LoadStore屏障。
當(dāng)讀線程的數(shù)量大大超過寫線程時(shí),選擇在
volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。
鎖的內(nèi)存語義
鎖是Java并發(fā)編程中最重要的同步機(jī)制。鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向獲取同一個(gè)鎖的線程發(fā)送消息。
當(dāng)線程釋放鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。和 volatile 寫 類似。
當(dāng)線程獲取鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效。和 volatile 讀 類似。
鎖釋放和鎖獲取的內(nèi)存語義總結(jié):
- 線程A釋放一個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對(duì)共享變量所做修改的)消息。
- 線程B獲取一個(gè)鎖,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共享變量所做修改的)消息。
- 線程A釋放鎖,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
類似 Java并發(fā)編程基礎(chǔ) 介紹的 等待/通知 機(jī)制。
final域的內(nèi)存語義
與前面介紹的鎖和volatile相比,對(duì)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è)操作之間不能重排序。
寫final域 的重排序規(guī)則禁止把final 域的寫重排序到構(gòu)造函數(shù)之外。
讀 final域 的重排序規(guī)則是,在一個(gè)線程中,初次讀對(duì)象引用與初次讀該對(duì)象包含的 final域,JMM禁止處理器重排序這兩個(gè)操作(注意,這個(gè)規(guī)則僅僅針對(duì)處理器)。
happens-before
happens-before 是 JMM 最核心的概念。
重排序規(guī)則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。
happens-before關(guān)系的定義如下:
- 如果一個(gè)操作
happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。 - 兩個(gè)操作之間存在
happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。
《JSR-133:Java Memory Model and Thread Specification》 定義了如下happens-before規(guī)則:
- 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,
happens-before于該線程中的任意后續(xù)操作。 - 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,
happens-before于隨后對(duì)這個(gè)鎖的加鎖。 - volatile變量規(guī)則:對(duì)一個(gè)
volatile 域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile 域的讀。 - 傳遞性:如果A
happens-beforeB,且Bhappens-beforeC,那么Ahappens-beforeC。 - start()規(guī)則:如果
線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。 - join()規(guī)則:如果
線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
雙重檢查鎖定與延遲初始化
雙重檢查鎖定 示例代碼:
private static Instance instance; //1
public static Instance getInstance() { //2
if (instance == null) { //3
synchronized (Instance.class) { //4
if (instance == null) { //5
instance = new Instance() //6
}
}
}
return instance;
}
存在的問題:
在線程執(zhí)行到第3行if (instance == null),代碼讀取到instance不為null時(shí),instance引用的對(duì)象有可能還沒有完成初始化。
問題的根源
前面的雙重檢查鎖定示例代碼的第6行instance=new Singleton();創(chuàng)建了一個(gè)對(duì)象。
這一行可以分解為:
memory = allocate(); // 1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory); // 2:初始化對(duì)象
instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址
代碼中的2和3之間,可能會(huì)被重排序?yàn)椋?/p>
memory = allocate(); // 1:分配對(duì)象的內(nèi)存空間
instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址
// 注意,此時(shí)對(duì)象還沒有被初始化!
ctorInstance(memory); // 2:初始化對(duì)象
如下圖所示,只要保證2排在4的前面,即使2和3之間重排序了,也不會(huì)違反intra-thread semantics。
如果發(fā)生重排序,另一個(gè)并發(fā)執(zhí)行的線程B就有可能在示例代碼第 3 行if (instance == null)判斷instance不為null。
解決方法:
- 不允許圖中
2和3重排序。 - 允許圖中
2和3重排序,但不允許其他線程“看到”這個(gè)重排序。
基于volatile的解決方案
只需要給變量 instance 添加 volatile 修飾符。
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (Instance.class) {
if (instance == null) {
instance = new Instance();
}
}
}
return instance;
}
基于類初始化的解決方案
public static class InstanceFactory {
public static Instance getInstance() {
// 這里將導(dǎo)致InstanceHolder類被初始化
return InstanceHolder.instance;
}
private static class InstanceHolder {
public static Instance instance = new Instance();
}
}
初始化一個(gè)類,包括執(zhí)行這個(gè)類的靜態(tài)初始化和初始化在這個(gè)類中聲明的靜態(tài)字段。根據(jù)Java語言規(guī)范,在首次發(fā)生下列任意一種情況時(shí),一個(gè)類或接口類型T將被立即初始化。
-
T是一個(gè)類,而且一個(gè)T類型的實(shí)例被創(chuàng)建。 -
T是一個(gè)類,且T中聲明的一個(gè)靜態(tài)方法被調(diào)用。 -
T中聲明的一個(gè)靜態(tài)字段被賦值。 -
T中聲明的一個(gè)靜態(tài)字段被使用,而且這個(gè)字段不是一個(gè)常量字段。 -
T是一個(gè)頂級(jí)類(Top Level Class,見Java語言規(guī)范的§7.6),而且一個(gè)斷言語句嵌套在T內(nèi)部被執(zhí)行。
在InstanceFactory示例代碼中,首次執(zhí)行getInstance()方法的線程將導(dǎo)致InstanceHolder類被初始化(符合第4條)。
由于Java語言是多線程的,多個(gè)線程可能在同一時(shí)間嘗試去初始化同一個(gè)類或接口。
因此,在Java中初始化一個(gè)類或者接口時(shí),需要做細(xì)致的同步處理。
Java語言規(guī)范規(guī)定,對(duì)于每一個(gè)類或接口C,都有一個(gè)唯一的初始化鎖LC與之對(duì)應(yīng)。從C到LC的映射,由JVM的具體實(shí)現(xiàn)去自由實(shí)現(xiàn)。JVM在類初始化期間會(huì)獲取這個(gè)初始化鎖,并且每個(gè)線程至少獲取一次鎖來確保這個(gè)類已經(jīng)被初始化過了。
Java內(nèi)存模型綜述
處理器的內(nèi)存模型
順序一致性內(nèi)存模型 是一個(gè) 理論參考模型,JMM和處理器內(nèi)存模型在設(shè)計(jì)時(shí)通常會(huì)以順序一致性內(nèi)存模型為參照。
在設(shè)計(jì)時(shí),JMM和處理器內(nèi)存模型會(huì) 對(duì)順序一致性模型做一些放松,因?yàn)槿绻耆凑枕樞蛞恢滦阅P蛠韺?shí)現(xiàn)處理器和JMM,那么很多的處理器和編譯器優(yōu)化都要被禁止,這對(duì)執(zhí)行性能將會(huì)有很大的影響。
根據(jù)對(duì)不同類型的讀/寫操作組合的執(zhí)行順序的放松,可以把常見處理器的內(nèi)存模型劃分為如下幾種類型:
- 放松程序中寫-讀操作的順序,由此產(chǎn)生了
Total Store Ordering內(nèi)存模型(簡(jiǎn)稱為 TSO)。 - 在上面的基礎(chǔ)上,繼續(xù)放松程序中寫-寫操作的順序,由此產(chǎn)生了
Partial Store Order內(nèi)存模型(簡(jiǎn)稱為 PSO)。 - 在前面兩條的基礎(chǔ)上,繼續(xù)放松程序中讀-寫和讀-讀操作的順序,由此產(chǎn)生了
Relaxed Memory Order內(nèi)存模型(簡(jiǎn)稱為 RMO)和 PowerPC 內(nèi)存模型。
從上圖中可知:
- 所有處理器內(nèi)存模型都允許
寫-讀重排序,因?yàn)槎际褂昧?寫緩存區(qū)。
由于寫緩存區(qū)僅對(duì)當(dāng)前處理器可見,這個(gè)特性導(dǎo)致當(dāng)前處理器可以比其他處理器先看到臨時(shí)保存在自己寫緩存區(qū)中的寫。 - 從上到下,模型由強(qiáng)變?nèi)?。越是追求性能的處理器,?nèi)存模型設(shè)計(jì)得會(huì)越弱。
由于常見的處理器內(nèi)存模型比JMM要弱,Java編譯器在生成字節(jié)碼時(shí),會(huì)在執(zhí)行指令序列的適當(dāng)位置插入 內(nèi)存屏障 來限制處理器的重排序。
各種內(nèi)存模型之間的關(guān)系
JMM 是一個(gè)語言級(jí)的內(nèi)存模型。
處理器內(nèi)存模型 是硬件級(jí)的內(nèi)存模型。
順序一致性內(nèi)存模型 是一個(gè)理論參考模型。
從下圖可以看出:
常見的4種 處理器內(nèi)存模型 比常用的3中 語言內(nèi)存模型 要 弱,
處理器內(nèi)存模型 和 語言內(nèi)存模型 都比 順序一致性內(nèi)存模型 要 弱。
同處理器內(nèi)存模型一樣,越是追求執(zhí)行性能的語言,內(nèi)存模型設(shè)計(jì)得會(huì)越弱。
JMM的內(nèi)存可見性保證
按程序類型,Java程序的內(nèi)存可見性保證可以分為下列3類:
-
單線程程序:不會(huì)出現(xiàn)內(nèi)存可見性問題。JMM為它們提供了最小安全性保障:線程執(zhí)行時(shí)讀取到的值,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(
0、null、false)。 - 正確同步的多線程程序:程序的執(zhí)行將具有順序一致性。這是JMM關(guān)注的重點(diǎn),JMM通過限制編譯器和處理器的重排序來為程序員提供內(nèi)存可見性保證。
-
未同步/未正確同步的多線程程序:JMM為它們提供了最小安全性保障:線程執(zhí)行時(shí)讀取到的值,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(
0、null、false)。
JSR-133對(duì)舊內(nèi)存模型的修補(bǔ)
JSR-133 對(duì) JDK 5 之前的舊內(nèi)存模型的修補(bǔ)主要有兩個(gè):
- 增強(qiáng)
volatile的內(nèi)存語義:限制volatile變量與普通變量的重排序,使volatile的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義。 - 增強(qiáng)
final的內(nèi)存語義:保證final引用不會(huì)從構(gòu)造函數(shù)內(nèi)逸出的情況下,final具有了初始化安全性。
小結(jié)
本文我們介紹了:
- 線程之后如何通信以及同步?
- 命令式編程的兩種通信機(jī)制:共享內(nèi)存 和 消息傳遞。
Java并發(fā)采用的是 共享內(nèi)存,通信時(shí)隱式進(jìn)行的。 - Java內(nèi)存模型的抽象結(jié)構(gòu)。 存儲(chǔ)在堆內(nèi)存的
實(shí)例域、靜態(tài)域和數(shù)組元素等才能在線程間共享。 - 三種類型的重排序。
- happens-before 規(guī)則
- volatile的特性、內(nèi)存語義以及實(shí)現(xiàn)。
- 鎖的內(nèi)存語義
- final 域的內(nèi)存語義
- 基于 volatile 和 類初始化 兩種方式來處理 單例模式 雙重檢查鎖定的優(yōu)化,以及出現(xiàn)問題的根源介紹。
- 對(duì)各內(nèi)存模型的介紹和對(duì)比。
以上
</article>