前言
本文并非按照書中目錄所寫酬凳,為自己讀后總結(jié)谋竖,個(gè)人覺得這本書有著比較深的學(xué)習(xí)價(jià)值红柱,在此致敬本書作者。
并發(fā)編程模型兩個(gè)關(guān)鍵問題
并發(fā)編程需要著手解決原子性蓖乘、有序性锤悄、可見性三個(gè)問題,這三個(gè)問題側(cè)重在線程通信與線程同步上嘉抒。針對(duì)于這兩個(gè)問題零聚,有兩種機(jī)制來保證: 共享內(nèi)存 | 消息傳遞。
共享內(nèi)存屏蔽通信細(xì)節(jié)些侍,但需要顯式指定線程同步順序隶症;消息傳遞由程序員主動(dòng)發(fā)送消息,顯式執(zhí)行線程通信岗宣,線程同步由于自帶發(fā)送順序蚂会,隱式進(jìn)行。
\ | 線程通信 | 線程同步 | 典型語(yǔ)言 |
---|---|---|---|
共享內(nèi)存 | 隱式 | 顯式 | Java |
消息傳遞 | 顯式 | 隱式 | Go |
原子性狈定、有序性颂龙、可見性
原子性:操作不可分割。CPU層面保證基礎(chǔ)指令的原子性纽什,對(duì)于復(fù)雜原子指令措嵌,比如交換指令CMPXCHG,采用總線鎖or緩存行鎖來保證原子性芦缰。需要注意的是企巢,32位操作系統(tǒng)不對(duì)64位數(shù)據(jù)寫入保證原子性,比如long類型或者double類型變量寫入让蕾。
有序性:涉及到的指令重排分三種浪规,編譯級(jí)指令重排(編譯器優(yōu)化)、指令級(jí)指令重排(CPU指令并行)探孝、 內(nèi)存系統(tǒng)指令重排(CPU讀/寫緩存區(qū))笋婿,單線程模型下,CPU與編譯器不會(huì)對(duì)有間接依賴的指令重排序顿颅。
可見性:針對(duì)上述三種指令重排缸濒,而引發(fā)線程之間的內(nèi)存可見性問題。
進(jìn)一步充電 緩存一致性協(xié)議之MESI
Java內(nèi)存模型的抽象結(jié)構(gòu)
JMM定義了共享變量存儲(chǔ)于主存之中粱腻,每個(gè)線程都有一個(gè)私有的本地內(nèi)存庇配,存儲(chǔ)共享變量的副本。這里的本地內(nèi)存是一個(gè)抽象的概念绍些,并不真實(shí)存在捞慌,它涵蓋了CPU高速緩存(L1,L2柬批,L3)啸澡、寫緩沖區(qū)、編譯器優(yōu)化等等氮帐。為了保證內(nèi)存可見锻霎,Java編譯器在生成指令序列的適當(dāng)位置插入內(nèi)存屏障。
JMM內(nèi)存屏障
JMM把內(nèi)存屏障指令分為4類揪漩,見下表旋恼。
上面這四個(gè)內(nèi)存屏障簡(jiǎn)單來說,Load用于讀取裝載數(shù)據(jù)奄容,Store用于存儲(chǔ)冰更,會(huì)保證前面的裝載or存儲(chǔ)<優(yōu)先于>后面的裝載or存儲(chǔ)
volatile內(nèi)存語(yǔ)義
當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程的本地內(nèi)存的共享變量值刷新到主存昂勒。
當(dāng)讀一個(gè)volatile變量時(shí)蜀细,JMM會(huì)把該線程的本地內(nèi)存置為無效,從主存獲取共享變量戈盈。
- volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后奠衔。
- volatile讀之后的操作不會(huì)變編譯器重排序到volatile讀之前谆刨。
- 當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀归斤,不能重排序痊夭。
為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,編譯器生成字節(jié)碼通過插入內(nèi)存屏障來禁止重排序
- 在每個(gè)volatile寫前面插入StoreStore屏障脏里,確保volatile寫之前的數(shù)據(jù)刷新到主存她我,并且不會(huì)重排序到volatile寫之后。
- 在每個(gè)volatile寫后面插入StoreLoad 屏障迫横,確保volatile寫與后續(xù)可能的volatile讀/寫操作重排序(這個(gè)開銷昂貴)番舆。
- 在每個(gè)volatile讀后面插入LoadLoad 屏障,確保volatile讀不會(huì)與后續(xù)的普通讀重排序矾踱。
- 在每個(gè)volatile讀后面插入LoadStore 屏障恨狈,確保volatile讀不會(huì)與后續(xù)的普通寫重排序。
比較有意思的是volatile寫之后的StoreLoad屏障呛讲,JMM可以選擇在每個(gè)volatile寫之后或者volatile讀之前插入StoreLoad屏障拴事,但由于通常共享變量讀多寫少,JMM最終選擇在volatile寫之后插入StoreLoad屏障圣蝎,來提供一定的性能提升刃宵。
上面內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺(tái)徘公,任意程序中都能保證volatile的正確語(yǔ)義牲证。JMM針對(duì)不同平臺(tái)不同代碼,會(huì)省略部分內(nèi)存屏障來做優(yōu)化关面。
鎖(ReentrantLock)的內(nèi)存語(yǔ)義
- 公平鎖與非公平鎖釋放時(shí)坦袍,都要寫volatile變量state。
- 公平鎖獲取時(shí)等太,首先會(huì)讀volatile變量捂齐。
- 非公平鎖獲取時(shí),首先CAS更新volatile變量缩抡。
編譯器會(huì)為CAS的交換指令CMPXCHG加入lock前綴奠宜,lock前綴同時(shí)具有volatile讀與volatile寫的內(nèi)存語(yǔ)義。
總結(jié)來說:加鎖具有和volatile讀相同的內(nèi)存語(yǔ)義瞻想,解鎖具有和volatile寫相同的內(nèi)存語(yǔ)義压真。
并發(fā)包下的大部分鎖,同步器都是基于AQS實(shí)現(xiàn)的蘑险,并發(fā)包的基石是volatile滴肿、synchronize、cas佃迄,JUC的包有個(gè)通用的實(shí)現(xiàn)模式:首先聲明共享變量為volatile泼差,然后使用CAS原子更新實(shí)現(xiàn)線程之間同步贵少,同時(shí)配合CAS或volatile讀寫的內(nèi)存語(yǔ)義來實(shí)現(xiàn)線程之間的通信。
final域重排序規(guī)則
- 編譯器會(huì)在final域?qū)懼蠖言担瑯?gòu)造函數(shù)返回之前插入StoreStore內(nèi)存屏障滔灶,禁止final域的寫重排序到構(gòu)造函數(shù)之外。
- 初次讀包含final域的對(duì)象引用套啤,再初次讀final域,禁止重排序随常。這兩個(gè)操作之間存在間接依賴潜沦,大多數(shù)處理器本身就不會(huì)重排序,但也有少部分的處理器允許間接依賴的關(guān)系進(jìn)行重排序绪氛。
final的語(yǔ)義保證了正確構(gòu)建的對(duì)象不需要使用同步唆鸡,其他線程都能看到正確的被初始化之后的值。
以下為錯(cuò)誤示例代碼枣察,final引用從構(gòu)造函數(shù)溢出
/**
* @author YuanChong
* @create 2020-03-29 18:50
* @desc final引用從構(gòu)造函數(shù)溢出示例
*/
public class FinalExample {
private final int data;
private static FinalExample ref;
private FinalExample(int data) {
this.data = data;
ref = this;
}
public static void instanceObject() {
new FinalExample(1);
}
/**
* 并發(fā)下争占,A線程執(zhí)行instanceObject,B線程執(zhí)行readFinal序目,B線程讀到的可能是0也可能是1
* @return
*/
public static int readFinal() {
return ref.data;
}
}
JMM屏蔽內(nèi)存模型細(xì)節(jié)
JMM提供了as-if-serial語(yǔ)義與happens-before原則保證程序的正確執(zhí)行臂痕。
happens-before提供給程序員易于理解,簡(jiǎn)單易懂的并發(fā)下內(nèi)存可見性保證猿涨。
as-if-serial語(yǔ)義保證了不管怎么重排序握童,單線程程序的執(zhí)行結(jié)果不能被改變。
需要注意的是叛赚,這兩種語(yǔ)義只是JMM對(duì)程序員的保證承諾澡绩,JMM只保證執(zhí)行結(jié)果,但具體是否涉及重排序還要看編譯器與處理器的優(yōu)化俺附。這是JMM在編譯優(yōu)化與簡(jiǎn)單易懂的內(nèi)存模型之間的一個(gè)權(quán)衡結(jié)果肥卡。因此,happens-before更應(yīng)該理解成生效可見于事镣,他與執(zhí)行順序無關(guān)步鉴。
- 程序順序原則:本線程的每個(gè)操作生效可見于后續(xù)發(fā)生的所有操作
- 鎖規(guī)則:當(dāng)前線程解鎖生效可見于后續(xù)其他線程的加鎖
- volatile規(guī)則:volatile寫生效可見于后續(xù)對(duì)volatile的讀
- 傳遞性規(guī)則:如果A happens-before B,B happens-before C璃哟,那么A happens-before C
- start規(guī)則:如果A線程執(zhí)行Thread.start()啟動(dòng)線程B唠叛,A線程的Thread.start()生效可見于B線程的后續(xù)操作
- join規(guī)則:如果A線程執(zhí)行Thread.join(),B線程的任意操作生效可見于A從Thread.join()中返回
我們結(jié)合happens-before的幾個(gè)原則沮稚,可以分析出線程同步代碼是否有可見性問題
比如A線程執(zhí)行Thread.start()啟動(dòng)B線程艺沼,A線程做的共享變量的修改生效可見于B線程,這是由順序性規(guī)則蕴掏,start規(guī)則障般,傳遞性規(guī)則同時(shí)推斷出來的调鲸。
樓主之前也分析過鎖的happens-before推斷,詳見從happen-before角度分析synchronized與lock的內(nèi)存可見性問題