volatile

1、volatile關(guān)鍵字主要有三方面作用

1、實(shí)現(xiàn)long/double類型變量的原子操作
2、防止指令重排序(內(nèi)存屏障實(shí)現(xiàn),JIT編譯器搞的)
3、實(shí)現(xiàn)變量的可見性(內(nèi)存屏障實(shí)現(xiàn))

2、解析“實(shí)現(xiàn)long/double類型變量的原子操作”

對(duì)于long和double原生數(shù)據(jù)類型都是占8個(gè)字節(jié),也就是64位, 對(duì)于Java的原生類型還有其它6種,為啥單單只提到long和double呢?因?yàn)槌诉@倆原生類型,其它的原生類型不管是變量的讀寫都是原子性的,比如說:int a = 1;,它就是一個(gè)原子操作,也就是線程安全的。但是??!對(duì)于long和double類型它們卻不是原子的,怎么理解,下面用一圖來闡述一下為啥不是原子的:

image.png

如圖,這里以long類型來進(jìn)行說明,總共是64位,而實(shí)際在地址上是分為低32位和高32位來表示的,對(duì)于計(jì)算機(jī),有32和64位的處理器,像32位的機(jī)器很顯然無法尋址到64位地址,那對(duì)于這樣的機(jī)器對(duì)于long類型是如何來處理的呢?比如

double a = 1.0;
寫讀的情況下的問題

(線程1寫到一半,線程2寫完了,線程1再寫另外一半)
它在寫入時(shí)是先寫入低32位的數(shù)字,再寫入高32位的數(shù)字,然后再將高低32組合起來就變成了最終值了,所以很明顯這種寫操作不是原子性的,分步驟了嘛,所以如果在多線程的環(huán)境下,此時(shí)非原子的操作就會(huì)產(chǎn)生問題了,下面來分析一下會(huì)產(chǎn)生啥問題:

image.png

此時(shí)又有一個(gè)線程來讀取long數(shù)據(jù),此時(shí)這個(gè)線程讀到的是新的低32位的數(shù)據(jù)+舊的高32位的數(shù)據(jù),是不是最終讀出來的結(jié)果肯定就不如預(yù)期了。

同時(shí)寫的情況下
image.png

好,此時(shí)線程2又準(zhǔn)備來寫低32位了,此時(shí)就變成這樣了:

image.png

此時(shí),線程1再準(zhǔn)備寫高32位數(shù)據(jù)時(shí),是不是整個(gè)數(shù)據(jù)就亂了,再讀的話,就是線程1和線程2的一個(gè)中間結(jié)果,這就是對(duì)于long和double這倆數(shù)據(jù)類型的一個(gè)非常嚴(yán)重的問題,此時(shí)要解決這個(gè)問題,就可以用volatile關(guān)鍵字聲明既可:

3、volatile關(guān)鍵字對(duì)硬件上的影響:

這是必須要理解的前提條件,這里再稍加闡述一下背景:在JVM當(dāng)中,如果不用volatile修飾變量的話,程序在讀取該變量時(shí)往往不會(huì)直接從內(nèi)存當(dāng)中讀取,而是從cpu的寄存器中讀取,因?yàn)榧拇嫫魇荂PU直接可以操縱最快的途徑,而內(nèi)存要比寄存器要慢得多,如果沒有volatile修飾的變量由于不是直接從內(nèi)存當(dāng)中讀取的,所以有可能讀取的值不是最新的值;而當(dāng)使用volatile修飾變量時(shí),應(yīng)用就不會(huì)從寄存器中獲取該變量的值,而是從內(nèi)存(高速緩存)中獲取,這樣就能保存每次讀取的都是最新的,因?yàn)橹苯邮菑膬?nèi)存中讀的,但是肯定會(huì)損失一些性能,畢境比從寄存器中讀要慢一些。

4、volatile跟鎖關(guān)系:

在有些文獻(xiàn)當(dāng)中將volatile關(guān)鍵字是一個(gè)“輕量級(jí)的鎖”,為啥?因?yàn)樵谀承﹫?chǎng)景下volatile關(guān)鍵字和鎖有一些類似的地方。類似的有以下兩點(diǎn):

1、確保變量的內(nèi)存可見性。

2、防止指令重排序。

volatile關(guān)鍵字和鎖都使用了內(nèi)存屏障實(shí)現(xiàn)的,對(duì)于synchronized代碼塊而言,對(duì)應(yīng)的字節(jié)碼指令

monitorenter
內(nèi)存屏障 (Acquire Barrier,獲取屏障)/ /防止monitorenter指令和下面代碼重排序
.....
內(nèi)存屏障 (Release Barrier,釋放屏障)/ /防止monitorexit指令和上面代碼重排序
monitorexit

既然類似那直接用volatile來實(shí)現(xiàn)鎖操作不就可以了么?其實(shí)還是有不同的點(diǎn)的:

1、相比于鎖,volatile可以確保對(duì)變量寫操作的原子性(一條CPU的指令),但是它不具備排他性(像synchronized關(guān)鍵字就有排他性,所謂排他性就是同一時(shí)間只能有一個(gè)線程進(jìn)行上鎖,其它線程只能進(jìn)行等待)。使用volatile關(guān)鍵字修飾一個(gè)變量時(shí),當(dāng)一個(gè)線程對(duì)這個(gè)變量進(jìn)行寫操作,同時(shí)其他的線程也可以對(duì)它進(jìn)行寫操作,這個(gè)語義可以確保修改這個(gè)變量是正確的

2、使用鎖可能會(huì)導(dǎo)致線程的上下文切換(內(nèi)核態(tài)與用戶態(tài)之間的切換),而使用volatile并不會(huì)出現(xiàn)這種情況。volatile始終處于用戶態(tài)狀態(tài)

5、volatile使用場(chǎng)景:

總結(jié):如果要實(shí)現(xiàn)volatile寫操作的原子性,那么在等號(hào)右側(cè)的賦值變量中就不能出現(xiàn)被多線程所共享的變量,哪怕這個(gè)變量也是volatile也不可以。

雖說volatile可以稱之為“輕量級(jí)的”鎖,但是?。∷荒苋〈i,因?yàn)樗陨碛幸恍╇y以解決的問題存在,什么問題呢?下面進(jìn)一步闡述一下:

int a = b + 2;

像上面這句代碼會(huì)產(chǎn)生幾個(gè)指令呢?其實(shí)是會(huì)產(chǎn)生兩個(gè)指令,第一個(gè)指令是b+1,而第二個(gè)指令是將b+1的值賦值給a,很明顯不是原子性的操作。那咱們用一下volatile唄:

volatile int a = b + 2;

這樣就能保證原子操作了么?no!!!!因?yàn)閷?duì)于等式右側(cè)的"b+2"這個(gè)可以被多個(gè)線程訪問,那a的值也就有不確定性了,如如果這樣修改呢?

volatile int b = 1;
volatile int a = b + 2;

也不行,雖說b是原子性了,但是“b + 2”還不是呀。那再看一個(gè)等式:

valatile int a = a++;

也不能確保a變量的原子性,因?yàn)閍++這本身就不是原子的,先加再賦值兩步操作,所以對(duì)于這種賦值操作右側(cè)不是原子性的情況不適合使用volatile,而正確的使用姿勢(shì)應(yīng)該是這樣:

volatile int count = 1;
volatile boolean flag = false;

下面再來看一個(gè)等式;

volatile Date date = new Date();

由于new Data()它背后是先在堆中開辟空間,然后最終返回一個(gè)引用賦值給變量date,也不是一個(gè)原子的,這里只能保證引用賦值操作是原子的,所以此時(shí)的volatile關(guān)鍵字保證不了原子性。

6、何為指令重排序?

這其實(shí)涉及到JIT(Just In Time)的一些功能,在現(xiàn)代化的JVM編譯器當(dāng)中,它會(huì)根據(jù)我們所寫的代碼的情況自動(dòng)的一定程序的優(yōu)化,其中優(yōu)化當(dāng)中就有一個(gè)可能就是會(huì)對(duì)咱們的指令進(jìn)行一定的修改,比如按照順序執(zhí)行了三條指令:1、2、3【對(duì)應(yīng)我們的代碼順序】,但是在編譯完之后可能生成的字節(jié)碼會(huì)變成3、2、1,或1、3、2等,也就是對(duì)指令進(jìn)行重排序了,這里用一個(gè)簡(jiǎn)單的例子來直觀的看一下指令重排序的大概思想:

int a = 0;
int b = 1;

a++;

重排后可能為:

int a = 0;
a++;
int b = 0;

對(duì)于這個(gè)重排序其實(shí)是編譯器為了讓我們的程序執(zhí)行的性能更高而采取的一種優(yōu)化手段,但是?。?!在極端情況下這種指令重排序的優(yōu)化手段并不是我們需要的,所以此時(shí)就需要防止某些指令重排序,而是按我們所編寫的代碼的順序來執(zhí)行。對(duì)于指令重排序而言,在單線程環(huán)境下肯定是沒任何問題的,如果有問題也不可能出現(xiàn)這種優(yōu)化策略了,重點(diǎn)是在多線程的環(huán)境下這種所謂優(yōu)化的指令重排序策略可能就會(huì)產(chǎn)生問題,而這個(gè)volatile關(guān)鍵字就具備這種防止指令重排序的功能。

7、闡述內(nèi)存屏障(memeory barrier):

對(duì)于volatile關(guān)鍵字變量的讀寫操作,本質(zhì)上都是通過內(nèi)存屏障來執(zhí)行的,而內(nèi)存屏障兼具了如下兩方面的能力:

  • 1、防止指令重排序。

  • 2、實(shí)現(xiàn)變量?jī)?nèi)存的可見性。

1、volatile寫入操作
int a = 1;
String s = "Hello";

內(nèi)存屏障 (Release Barrier,釋放屏障)

volatile boolean v = false; / /寫入操作

內(nèi)存屏障(Store Barrier,存儲(chǔ)屏障)
  • 釋放屏障:防止下面的volatile與上面的所有操作的指令重排序,即遇到該屏障,則會(huì)把它之前的所有代碼發(fā)布出去,其他的線程就能立馬看到最新的結(jié)果

  • 存儲(chǔ)屏障:它的重要作用是刷新處理器的緩存,結(jié)果是可以確保該存儲(chǔ)屏障之前一切的操作所生成的結(jié)果對(duì)于其他處理器來說都可見(注意:是包括當(dāng)前的寫入操作)

2、volatile讀取操作:
內(nèi)存屏障 (Load Barrier,加載屏障)
boolean v1 = v; / /讀取操作 ,前面有volatile boolean v = false;
內(nèi)存屏障 (Acquire Barrier,獲取屏障)

int a = 1;
String s = "Hello";
  • 加載屏障:可以刷新處理器緩存,同步其他處理器對(duì)該volatile變量的修改結(jié)果。(即讓v一定是最新的值,而非舊值。在volatile變量讀入到工作內(nèi)存時(shí),都會(huì)刷新處理器緩存)
  • 獲取屏障:可以防止上面的volatile讀取操作與下面的所有操作語句的指令重排序,則會(huì)把volatile讀取操作執(zhí)行的代碼發(fā)布出去,其他的線程就能立馬看到最新的結(jié)果(其實(shí)是其他CPU總線嗅探到共享變量的數(shù)據(jù)變化,CPU會(huì)將在工作內(nèi)存的該變量對(duì)應(yīng)的緩存行設(shè)置為無效狀態(tài), 當(dāng)CPU對(duì)該變量進(jìn)行讀取時(shí)需要重新往主內(nèi)存中再次讀取,相當(dāng)于可以看到最新的結(jié)果)
3、兩個(gè)操作的對(duì)比
image.png
image.png

最后總結(jié)一下:

1、對(duì)于讀取操作來說,volatile可以確保該操作與其后續(xù)的所有讀寫操作都不會(huì)進(jìn)行指令重排序。

2、對(duì)于修改操作來說,valatile可以確保該操作與其上面的所有讀寫操作都不會(huì)進(jìn)行指令重排序。

注意:

在上面的舉例中都是Java的原生數(shù)據(jù)類型

如果是一個(gè)引用類型呢?比如說ArrayList,那對(duì)于volatile的內(nèi)存屏障功效是不起作用的,為啥?因?yàn)锳rrayList中的讀寫操作都不是原子的,比如讀操作,得先找到元素的地址,然后再進(jìn)行讀取,但是?。∪绻麑rrayList的引用賦值給另一個(gè)volatile的ArrayList,這就可以確保原子操作,也就有了volatile相關(guān)的功效了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容