本文主要介紹JMM、線程安全需要滿足的三大原則颁井、happens-before規(guī)則厅贪。
一. 為什么要了解JMM(Java Memory Model
)
線程安全: 當(dāng)多個(gè)線程訪問同一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替運(yùn)行雅宾,也不需要進(jìn)行額外的同步养涮,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲取正確的結(jié)果眉抬,那這個(gè)對(duì)象是線程安全的贯吓。
出現(xiàn)線程安全的問題一般是因?yàn)?strong>主內(nèi)存和工作內(nèi)存數(shù)據(jù)不一致性和重排序導(dǎo)致的,而解決線程安全的問題最重要的就是理解這兩種問題是怎么來的蜀变,理解它們的核心在于理解java內(nèi)存模型(JMM)悄谐。
在多線程條件下,多個(gè)線程肯定會(huì)相互協(xié)作完成一件事情库北,一般來說就會(huì)涉及到多個(gè)線程間相互通信告知彼此的狀態(tài)以及當(dāng)前的執(zhí)行結(jié)果等爬舰,另外们陆,為了性能優(yōu)化,還會(huì)涉及到編譯器指令重排序和處理器指令重排序洼专。
Java內(nèi)存模型(即棒掠,簡(jiǎn)稱JMM)本身是一種抽象的概念,并不真實(shí)存在屁商,它描述的是一組規(guī)則或規(guī)范烟很,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問方式蜡镶。
由于JVM運(yùn)行程序的實(shí)體是線程雾袱,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為棧空間)官还,用于存儲(chǔ)線程私有的數(shù)據(jù)芹橡。
而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域望伦,所有線程都可以訪問林说,但線程對(duì)變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行
首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對(duì)變量進(jìn)行操作屯伞,操作完成后再將變量寫回主內(nèi)存腿箩,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝劣摇,前面說過珠移,工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無(wú)法訪問對(duì)方的工作內(nèi)存末融,線程間的通信(傳值)必須通過主內(nèi)存來完成
二. JMM是--內(nèi)存模型抽象結(jié)構(gòu)
我們知道CPU的處理速度和主存的讀寫速度不是一個(gè)量級(jí)的钧惧,為了平衡這種巨大的差距,每個(gè)CPU都會(huì)有緩存勾习。因此浓瞪,共享變量會(huì)先放在主存中,每個(gè)線程都有屬于自己的工作內(nèi)存巧婶,并且會(huì)把位于主存中的共享變量拷貝到自己的工作內(nèi)存追逮,之后的讀寫操作均使用位于工作內(nèi)存的變量副本,并在某個(gè)時(shí)刻將工作內(nèi)存的變量副本寫回到主存中去粹舵。JMM就從抽象層次定義了這種方式,并且JMM決定了一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)其他線程是可見的骂倘。
如圖為JMM抽象示意圖眼滤,線程A和線程B之間要完成通信的話,要經(jīng)歷如下兩步:
- 線程A從主內(nèi)存中將共享變量讀入線程A的工作內(nèi)存后并進(jìn)行操作历涝,之后將數(shù)據(jù)重新寫回到主內(nèi)存中诅需;
- 線程B從主存中讀取最新的共享變量
從橫向去看看漾唉,線程A和線程B就好像通過共享變量在進(jìn)行隱式通信。這其中有很有意思的問題堰塌,如果線程A更新后數(shù)據(jù)并沒有及時(shí)寫回到主存赵刑,而此時(shí)線程B讀到的是過期的數(shù)據(jù),這就出現(xiàn)了“臟讀”現(xiàn)象场刑“愦耍可以通過同步機(jī)制(控制不同線程間操作發(fā)生的相對(duì)順序)來解決或者通過volatile關(guān)鍵字使得每次volatile變量都能夠強(qiáng)制刷新到主存,從而對(duì)每個(gè)線程都是可見的牵现。
三. 內(nèi)存模型三大特性
1. 原子性
Java 內(nèi)存模型允許虛擬機(jī)將沒有被 volatile 修飾的 64 位數(shù)據(jù)(long铐懊,double)的讀寫操作劃分為兩次 32 位的操作來進(jìn)行,也就是說對(duì)這部分?jǐn)?shù)據(jù)的操作可以不具備原子性瞎疼。
AtomicInteger科乎、AtomicLong、AtomicReference 等特殊的原子性變量類提供了下面形式的原子性條件更新語(yǔ)句贼急,使得比較和更新這兩個(gè)操作能夠不可分割地執(zhí)行茅茂。
boolean compareAndSet(expectedValue, updateValue);
AtomicInteger 使用舉例:
private AtomicInteger ai = new AtomicInteger(0);
public int next() {
return ai.addAndGet(2)
}
也可以使用 synchronized 同步操作來保證操作具備原子性,它對(duì)應(yīng)的虛擬機(jī)字節(jié)碼指令為 monitorenter 和 monitorexit太抓。
2. 可見性
如果沒有及時(shí)地對(duì)主內(nèi)存與工作內(nèi)存的數(shù)據(jù)進(jìn)行同步空闲,那么就會(huì)出現(xiàn)不一致問題。如果存在不一致的問題腻异,一個(gè)線程對(duì)一個(gè)共享數(shù)據(jù)所做的修改就不能被另一個(gè)線程看到进副。
volatile 可以保證可見性,它
- 在
修改
一個(gè)共享數(shù)據(jù)時(shí)會(huì)將該值從工作內(nèi)存同步到主內(nèi)存 - 對(duì)一個(gè)共享數(shù)據(jù)進(jìn)行
讀取
時(shí)會(huì)先從主內(nèi)存同步到工作內(nèi)存
synchronized 也能夠保證可見性悔常,他能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼影斑,并且在釋放鎖之前會(huì)將對(duì)變量的修改刷新到主內(nèi)存當(dāng)中。
不過只有對(duì)共享變量的 set() 和 get() 方法都加上 synchronized 才能保證可見性机打,如果只有 set() 方法加了 synchronized矫户,那么 get() 方法并不能保證會(huì)從內(nèi)存中讀取最新的數(shù)據(jù)。(可見性問題残邀,參看下面博客)
3. 有序性
在 Java 內(nèi)存模型中皆辽,允許編譯器和處理器對(duì)指令進(jìn)行重排序,重排序過程不會(huì)影響到單線程程序的執(zhí)行芥挣,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性驱闷。
volatile 關(guān)鍵字通過添加內(nèi)存屏障的方式來禁止指令重排,即重排序時(shí)不能把后面的指令放到內(nèi)存屏障之前空免。
也可以通過 synchronized 來保證有序性空另,它保證每個(gè)時(shí)刻只有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼蹋砚,自然就保證了有序性扼菠。
四. 重排序
一個(gè)好的內(nèi)存模型實(shí)際上會(huì)放松對(duì)處理器和編譯器規(guī)則的束縛摄杂,也就是說軟件技術(shù)和硬件技術(shù)都為同一個(gè)目標(biāo)而進(jìn)行奮斗:在不改變程序執(zhí)行結(jié)果的前提下,盡可能提高并行度循榆。JMM對(duì)底層盡量減少約束析恢,使其能夠發(fā)揮自身優(yōu)勢(shì)。因此秧饮,在執(zhí)行程序時(shí)映挂,為了提高性能,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序浦楣。一般重排序可以分為如下三種:
- 編譯器優(yōu)化的重排序袖肥。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序振劳;
- 指令級(jí)并行的重排序∽底椋現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性历恐,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序寸癌;
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū)弱贼,這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行的蒸苇。
如圖,1屬于編譯器重排序吮旅,而2和3統(tǒng)稱為處理器重排序溪烤。這些重排序會(huì)導(dǎo)致線程安全的問題。
針對(duì)編譯器重排序庇勃,JMM的編譯器重排序規(guī)則會(huì)禁止一些特定類型的編譯器重排序檬嘀;
針對(duì)處理器重排序,編譯器在生成指令序列的時(shí)候會(huì)通過插入內(nèi)存屏障指令來禁止某些特殊的處理器重排序责嚷。
那么什么情況下鸳兽,不能進(jìn)行重排序了?下面就來說說數(shù)據(jù)依賴性罕拂。有如下代碼:
double pi = 3.14 //A
double r = 1.0 //B
double area = pi * r * r //C
這是一個(gè)計(jì)算圓面積的代碼揍异,由于A,B之間沒有任何關(guān)系,對(duì)最終結(jié)果也不會(huì)存在關(guān)系爆班,它們之間執(zhí)行順序可以重排序衷掷。因此可以執(zhí)行順序可以是A->B->C或者B->A->C執(zhí)行最終結(jié)果都是3.14,即A和B之間沒有數(shù)據(jù)依賴性柿菩。具體的定義為:如果兩個(gè)操作訪問同一個(gè)變量戚嗅,且這兩個(gè)操作有一個(gè)為寫操作,此時(shí)這兩個(gè)操作就存在數(shù)據(jù)依賴性這里就存在三種情況:1. 讀后寫;2.寫后寫渡处;3. 寫后讀,者三種操作都是存在數(shù)據(jù)依賴性的祟辟,如果重排序會(huì)對(duì)最終執(zhí)行結(jié)果會(huì)存在影響医瘫。編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性旧困,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴性關(guān)系的兩個(gè)操作的執(zhí)行順序
另外醇份,還有一個(gè)比較有意思的就是as-if-serial語(yǔ)義。
as-if-serial語(yǔ)義的意思是:不管怎么重排序(編譯器和處理器為了提供并行度)吼具,(單線程)程序的執(zhí)行結(jié)果不能被改變僚纷。編譯器,runtime和處理器都必須遵守as-if-serial語(yǔ)義拗盒。as-if-serial語(yǔ)義把單線程程序保護(hù)了起來怖竭,遵守as-if-serial語(yǔ)義的編譯器,runtime和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個(gè)幻覺:?jiǎn)尉€程程序是按程序的順序來執(zhí)行的陡蝇。比如上面計(jì)算圓面積的代碼痊臭,在單線程中,會(huì)讓人感覺代碼是一行一行順序執(zhí)行上登夫,實(shí)際上A,B兩行不存在數(shù)據(jù)依賴性可能會(huì)進(jìn)行重排序广匙,即A,B不是順序執(zhí)行的恼策。as-if-serial語(yǔ)義使程序員不必?fù)?dān)心單線程中重排序的問題干擾他們鸦致,也無(wú)需擔(dān)心內(nèi)存可見性問題。
五. happens-before規(guī)則
除了可以用 volatile 和 synchronized 來保證有序性涣楷。除此之外分唾,JVM 還規(guī)定了先行發(fā)生原則,讓一個(gè)操作無(wú)需控制就能先于另一個(gè)操作完成总棵。
主要有以下這些原則:
1. 單一線程原則-- Single thread rule
在一個(gè)線程內(nèi)鳍寂,在程序前面的操作先行發(fā)生于后面的操作。
2. 管程鎖定規(guī)則--Monitor Lock Rule
一個(gè) unlock 操作先行發(fā)生于后面對(duì)同一個(gè)鎖的 lock 操作情龄。
3. volatile 變量規(guī)則--Volatile Variable Rule
對(duì)一個(gè) volatile 變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作迄汛。
4. 線程啟動(dòng)規(guī)則-- Thread Start Rule
Thread 對(duì)象的 start() 方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
5. 線程加入規(guī)則--Thread Join Rule
join() 方法返回先行發(fā)生于 Thread 對(duì)象的結(jié)束骤视。
6. 線程中斷規(guī)則-- Thread Interruption Rule
對(duì)線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生鞍爱,可以通過 Thread.interrupted() 方法檢測(cè)到是否有中斷發(fā)生。
7. 對(duì)象終結(jié)規(guī)則-- Finalizer Rule
一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的 finalize() 方法的開始专酗。
8. 傳遞性-- Transitivity
如果操作 A 先行發(fā)生于操作 B睹逃,操作 B 先行發(fā)生于操作 C,那么操作 A 先行發(fā)生于操作 C。