volatile這個關鍵字可能很多朋友都聽說過,或許也都用過。在Java 5之前,它是一個備受爭議的關鍵字,因為在程序中使用它往往會導致出人意料的結果。在Java 5之后,volatile關鍵字才得以重獲生機。
volatile關鍵字雖然從字面上理解起來比較簡單,但是要用好不是一件容易的事情。講解volatile 之前, 我們先來了解了解並發編程中的三大特效,java內存模型
一. 並發編程中的三大特效(要想程序正確執行, 三者缺一不可)
1、原子性
原子性:即一個操作或者多個操作 要么全部執行並且執行的過程不會被任何因素打斷,要么就都不執行。(類似於ACID中的一致性)
2、可見性
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
3、有序性
有序性:即程序執行的順序按照代碼的先后順序執行
指令重排序: 一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的
Java內存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
下面就來具體介紹下happens-before原則(先行發生原則):
- 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在后面的操作
- 鎖定規則:一個unLock操作先行發生於后面對同一個鎖額lock操作
- volatile變量規則:對一個變量的寫操作先行發生於后面對這個變量的讀操作
- 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
- 線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作
- 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
- 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
- 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始
這8條原則摘自《深入理解Java虛擬機》。這8條規則中,前4條規則是比較重要的,后4條規則都是顯而易見的。
下面我們來解釋一下前4條規則:
對於程序次序規則來說,我的理解就是一段程序代碼的執行在單個線程中看起來是有序的。注意,雖然這條規則中提到“書寫在前面的操作先行發生於書寫在后面的操作”,這個應該是程序看起來執行的順序是按照代碼順序執行的,因為虛擬機可能會對程序代碼進行指令重排序。雖然進行重排序,但是最終執行的結果是與程序順序執行的結果一致的,它只會對不存在數據依賴性的指令進行重排序。因此,在單個線程中,程序執行看起來是有序執行的,這一點要注意理解。事實上,這個規則是用來保證程序在單線程中執行結果的正確性,但無法保證程序在多線程中執行的正確性。
第二條規則也比較容易理解,也就是說無論在單線程中還是多線程中,同一個鎖如果出於被鎖定的狀態,那么必須先對鎖進行了釋放操作,后面才能繼續進行lock操作。
第三條規則是一條比較重要的規則,也是后文將要重點講述的內容。直觀地解釋就是,如果一個線程先去寫一個變量,然后一個線程去進行讀取,那么寫入操作肯定會先行發生於讀操作。
第四條規則實際上就是體現happens-before原則具備傳遞性。
二. java內存模型
1、內存模型
大家都知道,計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。因此在CPU里面就有了高速緩存。
也就是,當程序在運行過程中,會將運算需要的數據從主存復制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之后,再將高速緩存中的數據刷新到主存當中
2.、什么是Java內存模型
Java虛擬機規范中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽掉各種硬件和操作系統的訪問差異,以實現讓Java程序在各種平台下都能達到一致的內存訪問效果。在此之前,主流程序語言(如C/C++等)直接使用物理硬件和操作系統的內存模型,因此,會由於不同平台上內存模型的差異,有可能導致程序在一套平台上並發完全正常,而在另外一套平台上並發訪問卻經常出錯,因此在某些場景下就不許針對不同的平台來編寫程序。
Java內存模型即要定義得足夠嚴謹,才能讓Java的並發內存訪問操作不會產生歧義;Java內存模型也必須定義地足夠寬松,才能使得虛擬機的實現有足夠的自由空間去利用硬件的各種特性來獲取更好的執行速度。經過長時間的驗證和修補,JDK1.5(實現了JSR-133)發布之后,Java內存模型已經成熟和完善起來了,一起來看一下。
3、主內存和工作內存
Java內存模型的主要目的是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。注意一下,此處的變量並不包括局部變量與方法參數,因為它們是線程私有的,不會被共享,自然也不會存在競爭,此處的變量應該是實例字段、靜態字段和構成數組對象的元素。
Java內存模型中規定了所有的變量都存儲在主內存中(如虛擬機物理內存中的一部分),每條線程還有自己的工作內存(如CPU中的高速緩存),線程的工作內存中保存了該線程使用到的變量到主內存的副本拷貝,線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之間無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成,線程、主內存和工作內存的交互關系如下圖所示:
內存模型跟java內存模型類似, CPU主內存相對應主內存, CPU高速緩存相對應java內存模型線程中的工作內存
緩存一致性問題:
多線程中會有, 例如所有內存中數據都為0,多線程進行+1操作. 線程1進行+1操作, 寫入工作內存, 主內存中. 這時候線程1的工作內存為1, 主內存也為1. 線程2再去寫操作, 如果正確執行的話, 應該是線程2去讀取獲取主內存中的值1, 然后再進行+1, =2. 但是也存在一種情況, 線程2直接拿工作內存中的緩存也就是0來+1, 再寫入主內存中, 這時候得到的值就為1了(這就是緩存一致性問題). 為什么呢? 因為線程1操作了, 線程2怎么會知道它已經操作了呢? 我自己工作內存中有緩存, 肯定是用我自己工作內存中的緩存啊
注意:
為了獲得較好的執行性能,Java內存模型並沒有限制執行引擎使用處理器的寄存器或者高速緩存來提升指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java內存模型中,也會存在緩存一致性問題和指令重排序的問題。
Java內存模型規定所有的變量都是存在主存當中(類似於前面說的物理內存),每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。
三.volatile(最輕量級的同步機制)
1、volatile關鍵字的兩層語義
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序。
也就是說volatile 會保證可見性, 有序性, 並不保證原子性
2、volatile的原理和實現機制
前面講述了源於volatile關鍵字的一些使用,下面我們來探討一下volatile到底如何保證可見性和禁止指令重排序的。
下面這段話摘自《深入理解Java虛擬機》:
“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”
lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
1)它確保指令重排序時不會把其后面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的后面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制將對緩存的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。
3、volatile 與 synchronized 的比較
(1)volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住;
(2)volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的;
(3)volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性;
(4)volatile不會造成線程的阻塞,即volatile不能用來同步,因為多個線程並發訪問volatile修飾的變量不會阻塞;synchronized可能會造成線程的阻塞;
(5)當一個域的值依賴於它之前的值時,volatile就無法工作了,如n=n+1,n++等。如果某個域的值受到其他域的值的限制,那么volatile也無法工作,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。
(6)volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化。
總結:
與鎖相比,volatile 變量是一種非常簡單但同時又非常脆弱的同步機制,它在某些情況下將提供優於鎖的性能和伸縮性。如果嚴格遵循 volatile 的使用條件即變量真正獨立於其他變量和自己以前的值 ,在某些情況下可以使用 volatile 代替 synchronized 來簡化代碼。然而,使用 volatile 的代碼往往比使用鎖的代碼更加容易出錯。