Java并發 --- volatile關鍵字

寫在前

在并發編程中,最需要處理的就是線程之間的通信和線程間的同步問題,JMM中可見性、原子性、有序性也是這兩個問題帶來的。volatile 是java虛擬機提供的輕量級的同步機制

在并發編程中,需要解決的兩個問題:

通信:在命令式編程中,線程之間的通信包括共享內存和消息傳遞 而 java并發采用的是共享內存模型,線程之間共享程序的公共狀態,通過讀寫內存總的公共狀態來隱式通信

JMM關于同步的規定:

1.線程解鎖前,必須把共享變量的值刷回主內存
2.線程加鎖前,必須讀取共享內存的最新值到自己的本地內存
3.加鎖解鎖是同一把鎖

volatile關鍵字主要作用

保證內存可見性

  • 我們已經知道Java 內存模型分為了主內存和工作內存兩部分,其規定程序所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(賦值、讀取等)都必須在工作內存中進行,而不能直接讀取主內存中的變量。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞都必須經過主內存的傳遞來完成。

  • 這樣就會存在一個情況,工作內存值改變后到主內存更新一定是需要一定時間的,所以可能會出現多個線程操作同一個變量的時候出現取到的值還是未更新前的值。這樣的情況我們通常稱之為「可見性」,而我們加上 volatile 關鍵字修飾的變量就可以保證對所有線程的可見性。

  • 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存

  • 當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效,線程接下來將會從主內存中讀取共享變量

為什么 volatile 關鍵字可以有可見性?

volatile是如何保證有序性呢?答案是內存屏障Memory Barrier

Memory Barrier 可以保證內存可見性和特定操作的執行順序

volatile寫操作之后都會插入一個store屏障,將工作內存中的值刷回到主內存,在讀操作之前都會插入一個load屏障,從主內存讀取最新的數據(可見性),而無論是stroe還是load都會告訴編譯器和cpu,屏障前后的指令都不要進行重排序優化(禁止指令重排)

  • 這得益于 Java 語言的先行發生原則(happens-before)。簡單地說,就是先執行的事件就應該先得到結果。但是! volatile 并不能保證并發下的安全。當一個線程修改了變量的值,新的值會立刻同步到主內存當中。而其他線程讀取這個變量的時候,也會從主內存中拉取最新的變量值

  • Java 里面的運算并非原子操作,比如 i++ 這樣的代碼,實際上,它包含了 3 個獨立的操作:讀取 i 的值,將值加 1,然后將計算結果返回給 i。這是一個「讀取-修改-寫入」的操作序列,并且其結果狀態依賴于之前的狀態,所以在多線程環境下存在問題。

    要解決自增操作在多線程下線程不安全的問題,可以選擇使用 Java 提供的原子類,如 AtomicInteger 或者使用 synchronized 同步方法。

原子性:在 Java 中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執行,要么不執行。也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量)才是原子操作。(變量之間的相互賦值不是原子操作,比如 y = x,實際上是先讀取 x 的值,再把讀取到的值賦值給 y 寫入工作內存)

禁止指令重排

什么是數據依賴性?

對同一數據的兩個操作中只要有一個是寫操作,那么就存在數據依賴性,比如寫后寫,讀后寫,寫后讀。

  • 指令重排:處理器為了提高程序效率,可能對輸入代碼進行優化,它不保證各個語句的執行順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
  • 指令重排是一把雙刃劍,雖然優化了程序的執行效率,但是在某些情況下,卻會影響到多線程的執行結果。

比如下面的代碼:

boolean contextReady = false;
//在線程A中執行:
context = loadContext();    // 步驟 1
contextReady = true;        // 步驟 2

//在線程B中執行:
while(!contextReady ){ 
   sleep(200);
}
doAfterContextReady (context);

以上程序看似沒有問題。線程 B 循環等待上下文 context 的加載,一旦 context 加載完成,contextReady == true 的時候,才執行 doAfterContextReady 方法。

但是,如果線程 A 執行的代碼發生了指令重排,也就是上面的步驟 1 和步驟 2 調換了順序,那線程 B 就會直接跳出循環,直接執行 doAfterContextReady() 方法導致出錯。而 volatile 采用「內存屏障」這樣的 CPU 指令就解決這個問題,不讓它指令重排。

使用 volatile 變量進行寫操作,匯編指令帶有 lock 前綴,相當于一個內存屏障,后面的指令不能重排到內存屏障之前。使用 lock 前綴引發兩件事:① 將當前處理器緩存行的數據寫回系統內存。②使其他處理器的緩存無效。相當于對緩存變量做了一次 store 和 write 操作,讓 volatile 變量的修改對其他處理器立即可見。

使用場景

從上面的總結來看,我們非常容易得出 volatile 的使用場景:

  1. 運行結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
  2. 變量不需要與其他的狀態變量共同參與不變約束。

比如下面的場景,就很適合使用 volatile 來控制并發,當 shutdown() 方法調用的時候,就能保證所有線程中執行的 work() 立即停下來。

volatile boolean shutdownRequest;
private void shutdown(){
    shutdownRequest = true;
}
private void work(){
    while (!shutdownRequest){
        // do something
    }
}

總結與補充

  • 對于 volatile 主要特性:保證可見性、禁止指令重排、解決 long 和 double 的 8 字節賦值問題。
  • 還有一個比較重要的是:它并不能保證并發安全(不能保證原子性),不要和 synchronized 混淆。

可以創建Volatile數組嗎?

  • Java 中可以創建 volatile類型數組,不過只是一個指向數組的引用,而不是整個數組。如果改變引用指向的數組,將會受到volatile 的保護,但是如果多個線程同時改變數組的元素,volatile標示符就不能起到之前的保護作用了。

volatile能使得一個非原子操作變成原子操作嗎?

雖然volatile只能保證可見性不能保證原子性,但用volatile修飾long和double可以保證其操作原子性。

  • 一種實踐是用 volatile 修飾 long 和 double 變量,使其能按原子類型來讀寫。double 和 long 都是64位寬,因此對這兩種類型的讀是分為兩部分的,第一次讀取第一個 32 位,然后再讀剩下的 32 位,這個過程不是原子的,但 Java 中 volatile 型的 long 或 double 變量的讀寫是原子的。
  • volatile 修復符的另一個作用是提供內存屏障(memory barrier),例如在分布式框架中的應用。簡單的說,就是當你寫一個 volatile 變量之前,Java 內存模型會插入一個寫屏障(write barrier),讀一個 volatile 變量之前,會插入一個讀屏障(read barrier)。意思就是說,在你寫一個 volatile 域時,能保證任何線程都能看到你寫的值,同時,在寫之前,也能保證任何數值的更新對所有線程是可見的,因為內存屏障會將其他所有寫的值更新到緩存。

volatile和synchronized的區別與聯系

  • 本質不同volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取,主要用于解決變量在多個線程之間的可見性問題;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住,只要解決多個線程訪問資源的同步性;

  • 作用域不同:volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的;

  • 是否原子性volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性;volatile不保證原子性的原因:線程A修改了變量還沒結束時,另外的線程B可以看到已修改的值,而且可以修改這個變量,而不用等待A釋放鎖,因為volatile 變量沒上鎖

  • 是否加鎖(阻塞)volatile不會造成線程的阻塞(沒有上鎖);synchronized可能會造成線程的阻塞;

  • 是否指令重排:volatile標記的變量不會被編譯器優化(即禁止指令重排);synchronized標記的變量可以被編譯器優化。

volatile可以保證線程安全?

單純使用 volatile 關鍵字是不能保證線程安全的!線程安全必須保證原子性,可見性,有序性。而volatile只能保證可見性和有序性!

  • volatile 只提供了一種弱的同步機制,用來確保將變量的更新操作通知到其他線程;
  • volatile 語義是禁用 CPU 緩存,直接從主內存讀、寫變量。語義表現為:更新(寫) volatile 變量時,JMM 會把線程對應的本地內存中的共享變量值刷新到主內存中;讀 volatile 變量時,JMM 會把線程對應的本地內存設置為無效,直接從主內存中讀取共享變量;
  • 當把變量聲明為 volatile 類型后,JVM 增加內存屏障,禁止 CPU 進行指令重排。

volatile底層的實現機制?

“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”,lock前綴指令實際上相當于一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

  • 它確保指令重排序時不會把其后面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的后面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
  • 它會強制將對緩存的修改操作立即寫入主存;
  • 如果是寫操作,它會導致其他CPU中對應的緩存行無效。

拓展:緩存一致性?

  • cpu和內存之間是有高速緩存的,一般分為多級。cpu首先是要從內存中讀取一個數據進緩存,然后從緩存中讀取進行操作,將結果返回給緩存,再把緩存寫回內存。

  • 比如:如果同一個變量i=0,有兩個線程執行i++方法,線程1把i從內存中讀取進緩存,而現在線程2也把i讀取進緩存,兩個線程執行完i++后,線程1寫回內存,i = 1,線程2也寫回內存i = 1,兩次++結果最終值為1,這就是著名的緩存一致性問題。為了解決這個問題,前人給了兩種方案:

巨人的肩膀

https://blog.csdn.net/yuyecsdn/article/details/103454244
http://www.lxweimin.com/p/7ca933a716a9
http://www.lxweimin.com/p/be5e7c355d78
https://www.javanav.com/interview/534e046986274288b71684704cb68162.html
https://blog.csdn.net/qq_33330687/article/details/80990729

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容