摘要:變量可見性問題的關(guān)鍵字保證了多個(gè)線程對(duì)變量值變化的可見性。只要一個(gè)線程需要首先讀取一個(gè)變量的值,基于這個(gè)值生成一個(gè)新值,則一個(gè)關(guān)鍵字不足以保證正確的可見性。
Java的volatile關(guān)鍵字用于標(biāo)記一個(gè)Java變量為“在主存中存儲(chǔ)”。更確切的說(shuō),對(duì)volatile變量的讀取會(huì)從計(jì)算機(jī)的主存中讀取,而不是從CPU緩存中讀取,對(duì)volatile變量的寫入會(huì)寫入到主存中,而不只是寫入到CPU緩存。
實(shí)際上,從Java5開始,volatile關(guān)鍵字不只是保證了volatile變量在主存中寫入和讀取,我回在后面的部分做相關(guān)的解釋。
變量可見性問題Java的volatile關(guān)鍵字保證了多個(gè)線程對(duì)變量值變化的可見性。這聽起來(lái)有點(diǎn)抽象,讓我來(lái)詳細(xì)解釋。
在一個(gè)多線程的程序中,當(dāng)多個(gè)線程操作非volatile變量時(shí),出于性能原因,每個(gè)線程會(huì)從主存中拷貝一份變量副本到一個(gè)CPU緩存中。如果你的計(jì)算機(jī)有多于一個(gè)CPU,每個(gè)線程可能會(huì)在不同的CPU中運(yùn)行。這意味著每個(gè)簡(jiǎn)稱拷貝變量到不同CPU的緩存中,如下圖:
對(duì)于非volatile變量,并沒有保證何時(shí)JVM從主存中讀取數(shù)據(jù)到CPU緩存,或者從CPU緩存中寫出數(shù)據(jù)到主存。這會(huì)導(dǎo)致一些問題。
想象一種情況,多于一個(gè)線程訪問一個(gè)共享對(duì)象,這個(gè)共享對(duì)象包含一個(gè)計(jì)數(shù)變量如下聲明:
public class ShareObject { public int counter = 0; }
考慮只有一個(gè)線程Thread1增加counter這個(gè)變量的值,但是Tread1和Thread2可能有時(shí)會(huì)讀取counter變量。
如果counter變量沒有被聲明為volatile,就不能保證何時(shí)這個(gè)變量的值會(huì)從CPU緩存寫回主存,這意味著,在CPU緩存中的counter變量的值可能和主存中的不一樣。如下圖所示:
線程沒有看到一個(gè)變量最新更新的值的原因是這個(gè)變量還沒有被一個(gè)線程寫回到主存,這被稱為“可見性”問題。一個(gè)線程對(duì)變量的更新對(duì)其他線程不可見。
Java的volatile可見性保證Java的volatile關(guān)鍵字想要解決變量可見性問題。通過聲明counter變量為volatile,所有對(duì)counter變量的寫入都回立即寫回到主存,同時(shí)所有對(duì)counter變量也都會(huì)從主存中讀取。
西面的代碼展示了如何把counter變量聲明為volatile:
public class SharedObject { public volatile int counter = 0; }
聲明一個(gè)變量為volatile保證了對(duì)變量的寫入對(duì)其他線程的可見性。
在上面的場(chǎng)景中,一個(gè)線程(T1)修改了counter變量的值,另一個(gè)線程(T2)讀取counter變量(但是不修改它),聲明counter變量為volatile足以保證對(duì)counter變量的寫入對(duì)T2可見。
但是,如果T1和T2都去增加counter變量的只,name聲明counter變量為volatile是不夠的,后面會(huì)說(shuō)明。
全volatile可見性保證實(shí)際上,Java的volatile的可見性保證不止volatile變量本身??梢娦员WC如下:
如果線程A寫一個(gè)volatile變量,線程B隨后讀取這個(gè)volatile變量,那么在寫這個(gè)volatile變量之前對(duì)線程A可見的所有變量,在線程B讀取這個(gè)volatile變量之后對(duì)線程B也可見。
如果線程A讀取一個(gè)volatile變量,那么當(dāng)A讀取這個(gè)volatile變量時(shí)所有對(duì)線程A可見的變量也可以從主存中再次讀取。
我用下面的代碼來(lái)說(shuō)明:
public class MyClass { private int years; private int months; private volatile int days; public void update(int years, int months, int days) { this.years = years; this.months = months; this.days = days; } }
update()方法寫入三個(gè)變量,只有days變量是volatile的。
全volatile可見性保證的意思是,當(dāng)一個(gè)值寫入到days變量,則所有對(duì)當(dāng)前線程可見的變量也會(huì)都寫入到主存,也就是當(dāng)一個(gè)值寫入到days變量,則years和months的只也被寫入到主存。
當(dāng)讀取years,months和days的值,可以這樣做:
public class MyClass { private int years; private int months; private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days) { this.years = years; this.months = months; this.days = days; } }
需要注意的是totalDays()方法起始于讀取days的值到total變量中。當(dāng)讀取days的值時(shí),months和years的值也被讀取到主存。因此可以保證你看到的是days,months和years的最新的值,前提是保證上面的讀取順序。
指令重排序挑戰(zhàn)出于性能的考量,JVM和CPU允許對(duì)程序中的指令進(jìn)行重排序,只要指令的語(yǔ)義不變。例如下面的指令:
int a = 1; int b = 2; a++; b++;
這些指令可以按照下面的順序重排,并不會(huì)丟失程序的語(yǔ)義:
int a = 1; a++; int b = 2; b++;
但是,指令重排序?qū)τ谄渲幸粋€(gè)變量是volatile變量這種情況是有挑戰(zhàn)的。讓我們看一下MyClass這個(gè)類:
public class MyClass { private int years; private int months; private volatile int days; public void update(int years, int months, int days) { this.years = years; this.months = months; this.days = days; } }
一旦update()方法對(duì)days變量寫入一個(gè)值,years和months新寫入的只也刷入到主存,但是,如果有JVM指令重排序,像下面這樣:
public void update(int years, int months, int days) { this.days = days; this.months = months; this.years = years; }
months和years的只在days變量修改的情況下依然會(huì)寫入到主存,但是這時(shí)將years和days變量值刷入主存這件事發(fā)生在對(duì)months和years寫入新值之前,則對(duì)years和days的更新對(duì)其他線程來(lái)說(shuō)就不可見了。這下指令重排序就改變了程序的語(yǔ)義。
Java有一個(gè)應(yīng)對(duì)此問題的解決方案,下面會(huì)講到。
Java的volatile的Happens-Before保證為了解決指令重排序的挑戰(zhàn),Java的volatile關(guān)鍵字除了可見性保證之外,給出了一個(gè)“happens-before”的保證。happens-before保證如下情況:
如果讀取和寫入其他非volatile變量發(fā)生在寫入volatile變量之前(這種情況這些非volatile變量也會(huì)被刷入主存),則讀取和寫入這些變量不能被重排序?yàn)榘l(fā)生在寫入這個(gè)volatile變量之后(禁止指令重排序)。在寫入一個(gè)volatile變量之前的讀取和寫入非volatile變量被保證為“happen before”寫入這個(gè)volatile變量。需要注意的是,例如在寫入一個(gè)volatile變量之后讀寫其他變量可以被重排序到寫入這個(gè)volatile變量之前。從“之后”重排序到”之前“是允許的,但是從”之前“重排序到”之后“是禁止的。
如果讀寫其他非volatile變量發(fā)生在讀取一個(gè)volatile變量之后(這種情況這些非volatile變量也會(huì)被刷到主存),則讀寫這些變量不能被重排序?yàn)榘l(fā)生在讀取這個(gè)volatile變量之前。需要注意的是,讀取其他變量發(fā)生在讀取一個(gè)volatile變量之前能夠被重排序?yàn)榘l(fā)生在讀取這個(gè)volatile變量之后。從”之前“重排序到“之后”是允許的,但是從“之后”重排序到“之前”是被禁止的。
上面的happens-before保障保證的volatile關(guān)鍵字的可見性是強(qiáng)制的。
volatile不總是足夠的盡管volatile關(guān)鍵字保證了所有對(duì)一個(gè)volatile變量的讀取都是從主存中讀取,所有對(duì)volatile關(guān)鍵字的寫入都是直接到主存,但是仍有其他情況使得聲明一個(gè)變量為volatile是不足夠的。
在前面解釋的情況,也就是只有Thread1寫共享變量counter,聲明counter變量為volatile足以保證Thread2總是看到最新寫入的值。
實(shí)際上,多線程都可以寫一個(gè)共享的volatile變量,并且仍然在主存中存儲(chǔ)正確的值,前提是寫入變量的新值不依賴于它之前的值。也就是說(shuō),如果一個(gè)線程寫入一個(gè)值到共享的volatile變量不需要先去讀它的值去產(chǎn)出下一個(gè)值。
只要一個(gè)線程需要首先讀取一個(gè)volatile變量的值,基于這個(gè)值生成一個(gè)新值,則一個(gè)volatile關(guān)鍵字不足以保證正確的可見性。在讀取volatile變量然后寫入新值的短暫的間隙,會(huì)產(chǎn)生競(jìng)態(tài)條件(race condition),這時(shí)多個(gè)線程可能讀取到相同的volatile變量的值,生成這個(gè)變量的新值,當(dāng)將新值寫回主存時(shí),會(huì)覆蓋彼此的值。
多線程增加相同計(jì)數(shù)器的值就是這種情況,導(dǎo)致一個(gè)volatile聲明不足夠。下面詳細(xì)解釋這種情況。
想象如果Thread1讀取一個(gè)值為0的共享的counter變量到它的CPU緩存,增加1并且不將這個(gè)改變的值寫回主存。Thread2然后從主存中讀取相同的值仍為0counter變量到它的CPU緩存。Thread2也為它增加1,也不寫回主存。這種情況如下圖所示:
Thread1和Thread2此時(shí)實(shí)際上已經(jīng)不同步了。共享變量counter的值應(yīng)該為2,但是每個(gè)線程在CPU緩存中的這個(gè)變量的值都為1,在主存中的值仍為0,這就亂了!盡管這兩個(gè)線程最終會(huì)將值寫回主存中的共享變量,這個(gè)值也是不正確的。
何時(shí)volatile是足夠的?正如前面所說(shuō),如果兩個(gè)線程都去讀寫同一個(gè)共享變量,只對(duì)這個(gè)共享變量使用volatile關(guān)鍵字是不夠的。你需要使用一個(gè)synchronized關(guān)鍵字去保證讀寫相同變量是原子的。讀寫一個(gè)volatile變量不會(huì)阻塞線程的讀寫。
作為synchronized塊替代方法,你可以使用java.util.concurrent包中的眾多原子數(shù)據(jù)類型。比如,AtomicLong或者AtomicReference或其他的類型。
只有一個(gè)線程讀寫一個(gè)volatile變量值,其他線程只讀取變量,則這些讀線程能夠保證看到寫入這個(gè)volatile變量的最新值,如果不聲明為volatile,則這種情況不能保證。
volatile的性能考量讀寫volatile變量會(huì)導(dǎo)致變量被讀寫到主存。讀寫主存比訪問CPU緩存開銷更大。訪問volatile變量也會(huì)禁止指令重排序,而指令重排序是一個(gè)正正常的性能優(yōu)化技術(shù)。因此,你應(yīng)該只在真正需要保證變量可見性的時(shí)候使用volatile變量。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/75950.html
摘要:三關(guān)鍵字能保證原子性嗎并發(fā)編程藝術(shù)這本書上說(shuō)保證但是在自增操作非原子操作上不保證,多線程編程核心藝術(shù)這本書說(shuō)不保證。多線程訪問關(guān)鍵字不會(huì)發(fā)生阻塞,而關(guān)鍵字可能會(huì)發(fā)生阻塞關(guān)鍵字能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性。 系列文章傳送門: Java多線程學(xué)習(xí)(一)Java多線程入門 Java多線程學(xué)習(xí)(二)synchronized關(guān)鍵字(1) java多線程學(xué)習(xí)(二)synchroniz...
摘要:前半句是指線程內(nèi)表現(xiàn)為串行的語(yǔ)義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國(guó)內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過這三篇文章,包括相關(guān)書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...
摘要:前半句是指線程內(nèi)表現(xiàn)為串行的語(yǔ)義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國(guó)內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過這三篇文章,包括相關(guān)書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...
摘要:前半句是指線程內(nèi)表現(xiàn)為串行的語(yǔ)義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國(guó)內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過這三篇文章,包括相關(guān)書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...
摘要:舉個(gè)例子,在多線程不使用環(huán)境中,每個(gè)線程會(huì)從主存中復(fù)制變量到緩存以提高性能。保證了變量的可見性關(guān)鍵字解決了變量的可見性問題。在多線程同時(shí)共享變量的情形下,關(guān)鍵字已不足以保證程序的并發(fā)性。 volatile 關(guān)鍵字能把 Java 變量標(biāo)記成被存儲(chǔ)到主存中。這表示每一次讀取 volatile 變量都會(huì)訪問計(jì)算機(jī)主存,而不是 CPU 緩存。每一次對(duì) volatile 變量的寫操作不僅會(huì)寫到 ...
閱讀 665·2023-04-26 02:58
閱讀 2387·2021-09-27 14:01
閱讀 3689·2021-09-22 15:57
閱讀 1272·2019-08-30 15:56
閱讀 1106·2019-08-30 15:53
閱讀 864·2019-08-30 15:52
閱讀 794·2019-08-26 14:01
閱讀 2237·2019-08-26 13:41