一顷蟆、概述
volatile關鍵字被稱為“輕量級 synchronized”诫隅,是Java中針對并發(fā)同步訪問提供的一種免鎖機制,只能用于修飾變量帐偎,不能用于方法及代碼塊等逐纬。volatile 可以保證共享變量的可見性。在介紹volatile關鍵字之前削樊,我們先來回憶下Java并發(fā)編程中的幾個概念豁生。
1. 原子性
這個其實在學習數據庫的時候就了解到了,所謂的原子性漫贞,也就是某個操作或多個操作甸箱,在執(zhí)行的時候要么全部執(zhí)行,要么全部不執(zhí)行迅脐,這個就是所謂的原子性芍殖。而 volatile 并不能保證原子性。
2. 可見性
要說可見性的話谴蔑,先要簡單說下內存模型豌骏;等內存模型說過之后,可見性就比較清楚了树碱。
2.1 操作系統內存模型
學過計算機組成原理的應該都知道肯适,計算機中程序的執(zhí)行都是通過CPU來控制的,在程序在執(zhí)行過程中成榜,勢必會涉及到數據的讀寫框舔,而這些數據是存儲在內存中的。而在計算機中赎婚,內存的速度是低于CPU的刘绣,因此如果只通過內存與CPU交互的話,勢必會影響到指令執(zhí)行的速度挣输,這時候自然就有了高速緩存存儲器纬凤,也就是高速緩存。
高速緩存是為了解決CPU和內存之間速度不匹配而采用的一項技術撩嚼。
當程序運行過程中停士,操作系統會將相應的數據從主存復制一份到CPU的高速緩存中挖帘,然后CPU進行計算時就可以直接從對高速緩存進行讀寫,當計算結束之后恋技,再將高速緩存中的數據刷新到主存當中拇舀。
不過在多線程中,這樣的處理方式是有問題的蜻底。在多核CPU中骄崩,每個線程可能會屬于不同的CPU,因此每個線程運行時有自己的高速緩存薄辅,如果多個線程同時操作某一個變量要拂,這時候就可能會出現預期之外的結果。假如有2個線程對某一個變量執(zhí)行加1操作:i = i + 1
; 初始時i的值為0站楚,那么有可能會出現下面這種情況:
初始時脱惰,兩個線程分別讀取i的值存入各自所在的CPU的高速緩存當中,然后線程1進行加1操作源请,然后把i的最新值1寫入到內存枪芒;此時線程2的高速緩存當中i的值還是0,進行加1操作之后谁尸,i的值為1舅踪,然后線程2把i的值寫入內存;
而最終結果i的值是1良蛮,而不是2抽碌,這就是所謂的緩存一致性問題,通常稱這種被多個線程訪問的變量為共享變量决瞳。也就是說货徙,如果一個變量在多個CPU中都存在緩存,那么就可能存在緩存不一致的問題皮胡。
而要解決這個問題痴颊,可以是 共享變量在被修改后,想辦法通知其他CPU將該該變量的緩存設置為失效狀態(tài)屡贺,這樣當其他CPU需要讀取這個值時蠢棱,發(fā)現緩存中緩存的變量是無效的,那么它將會從內存中重新讀取甩栈。
2.2 Java內存模型
Java虛擬機規(guī)范中試圖定義一種Java內存模型(Java Memory Model泻仙,JMM)來屏蔽各個硬件平臺和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果量没。
Java內存模型規(guī)定所有的變量都是存在主存當中(類似于前面說的物理內存)玉转,每個線程都有自己的工作內存(類似于前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行殴蹄,而不能直接對主存進行操作究抓。并且每個線程不能訪問其他線程的工作內存猾担,而工作內存中則保存了被該線程使用到的變量的主內存拷貝。
這里只簡單介紹下Java內存模型漩蟆,因為這其實屬于JVM的知識垒探,后續(xù)學習JVM的時候再仔細了解,這里只需要了解到Java的內存模型類似于操作系統的內存模型即可怠李。
2.3 可見性
了解了內存模型后,我們基本上就理解了可見性的概念了蛤克。所謂的可見性捺癞,是說當多個線程訪問同一個變量時(共享變量),一個線程修改了這個變量的值构挤,其他線程能夠立即看到修改后的值髓介。
- 而Java中的volatile關鍵字就是用來保證可見性的;當一個共享變量被volatile修飾時筋现,它會保證修改的值會立即被更新到主存唐础,并且CPU會讓使用此變量的工作內存中的拷貝失效,需要讀取時重新從主存讀确伞一膨;
- 而普通的共享變量不能保證可見性,因為普通共享變量被修改之后洒沦,什么時候被寫入主存是不確定的豹绪,當其他線程去讀取時,此時內存中可能還是原來的舊值申眼,因此無法保證可見性瞒津。
另外,關于可見性而導致問題的例子可以參考:可見性問題實例-并發(fā)編程網
3. 有序性
有序性括尸,簡單來說就是程序執(zhí)行的順序是按照代碼的先后順序來執(zhí)行巷蚪。但在代碼執(zhí)行的過程中,編譯器也好濒翻,CPU也好屁柏,在保證程序最終結果的前提下,會對代碼的執(zhí)行進行重新排序肴焊。并且最近這些年前联,計算機性能的提升在很大程序上都要歸功于這些重新排序的措施,而這些重新排序的方式又被稱為指令重排序娶眷。
3.1 指令重排序
指令重排序似嗤,一般來說,處理器為了提高程序運行效率届宠,可能會對輸入代碼進行優(yōu)化烁落,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致乘粒,但是它會保證程序最終執(zhí)行結果和代碼順序執(zhí)行的結果是一致的。
當然伤塌,處理器在進行重排序時會考慮指令之間的數據依賴性灯萍,如果某個指令2的執(zhí)行依賴于指令1,那么指令2一定會排到指令1之后每聪。雖然指令重排序不會影響到單線程程序的執(zhí)行旦棉,卻會影響到多線程并發(fā)執(zhí)行的正確性。
3.2 Happens-Before關系
Java內存模型為程序中所有的操作定義了一個偏序關系药薯,稱之為 Happens-Before绑洛。比如兩個操作A和操作B,要想保證執(zhí)行操作B的線程能看到執(zhí)行操作A的結果(無論A和B是否在同一個線程中執(zhí)行)童本,那么在A和B之間必須滿足Happens-Before關系真屯,如果兩個操作之間不符合Happens-Before關系,那么JVM可以對他們任意地重排序穷娱。
Java內存模型可以不通過任何手段“天然的”就能保證有序性,只要操作滿足Happens-Before原則泵额;如果兩個操作之間的關系不在下面幾個規(guī)則中配深,并且也無法從下列規(guī)則中推導出來梯刚,那么JVM就可以對他們隨意重排序凉馆;
Happens-Before的規(guī)則包括(這里參考自《Java并發(fā)編程實戰(zhàn)》):
- 程序順序規(guī)則:同一個線程內澜共,如果操作A在操作B之前,那么在線程中A操作將在B操作之前執(zhí)行京革;
- 監(jiān)視器鎖規(guī)則:在監(jiān)視器鎖上的解鎖操作必須在同一個監(jiān)視器鎖上的加鎖操作之前執(zhí)行,這里指的是同一個鎖幸斥;
- volatile變量規(guī)則:對volatile變量的寫入操作必須在對該變量的讀操作之前執(zhí)行甲葬;
- 線程啟動規(guī)則:在線程上對Thread.start的調用必須在該線程中執(zhí)行任何操作之前執(zhí)行廊勃;
- 線程結束規(guī)則:線程中的任何操作都必須在其他線程檢測到該線程已經結束之前執(zhí)行,或者從Thread.join中成功返回经窖,或者在調用Thread.isAlive時返回false坡垫;
- 線程中斷規(guī)則:當一個線程在另一個線程上調用interrupt時梭灿,必須在被中斷線程檢測到interrupt調用之前執(zhí)行(通過拋出InterruptedException,或者調用isInterrupted和interrupted)冰悠;
- 對象終結規(guī)則:一個對象的初始化完成(構造函數執(zhí)行結束)必須在該對象的finalize() 方法之前執(zhí)行完成堡妒;
- 傳遞性:如果A before B執(zhí)行,B before C執(zhí)行溉卓,那么A必須在C之前執(zhí)行皮迟;
二、volatile 關鍵字
前面基本上了解了學習volatile關鍵字的一些前提條件的诵,接下來我們來學習下volatile關鍵字万栅。
1. volatile特性
被 volatile 修飾的變量,具有兩個特性:
a. 保證了該變量對于不同線程之間操作時的可見性西疤;
b. 禁止指令重排序;
2. 典型用法
一般情況下休溶,我們使用volatile的典型用法就是:檢查某個標記以判斷是否退出循環(huán)代赁,比如下面的代碼:
private boolean asleep;
while (!asleep) {
countSomeSheep(); // 數綿羊
}
//other
asleep = false;
首先,我們沒有使用volatile來修飾兽掰,但是這樣的話芭碍,就會有可見性問題,也就是當asleep被另一個線程修改時孽尽,執(zhí)行判斷的線程卻發(fā)現不了窖壕,詳細可以參考上面鏈接中的可見性問題參考實例。不過我們使用volatile修飾之后就變得不一樣了:private volatile boolean asleep;
- 首先杉女,volatile關鍵字會將修改后的變量強制寫入內存瞻讽;
- 并且會導致其他線程的工作內存中該變量的拷貝緩存失效;
- 其他線程再次讀取該變量時將會去主存中讀妊妗速勇;
3. 如何禁止指令重排序
首先,我們來看下對于普通變量和volatile變量操作時有什么區(qū)別:
- 普通變量坎拐,讀取數據時會先讀取工作內存的數據烦磁,如果工作內存中不存在,則從主內存中拷貝一份數據到工作內存中哼勇;寫操作只會修改工作內存的副本數據都伪,這種情況下,其它線程就無法讀取變量的最新值积担;
- 對于volatile變量陨晶,讀取數據時JMM會把工作內存中對應的值設為無效,要求線程從主內存中讀取數據磅轻;寫操作時JMM會把工作內存中對應的數據刷新到主內存中珍逸,這種情況下逐虚,其它線程就可以讀取變量的最新值;
volatile變量的內存可見性是基于內存屏障(Memory Barrier)實現的谆膳,什么是內存屏障叭爱?內存屏障,又稱內存柵欄漱病,是一個CPU指令买雾。在程序運行時,為了提高執(zhí)行性能杨帽,編譯器和處理器會對指令進行重排序漓穿,JMM為了保證在不同的編譯器和CPU上有相同的結果,通過插入特定類型的內存屏障來禁止特定類型的編譯器重排序和處理器重排序注盈,插入一條內存屏障會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序晃危。
a = 1; // 1
b = 2; // 2
instance = new Singleton(); // 3
c = a + b; // 4
- 如果變量instance沒有volatile修飾,指令1老客、2僚饭、3、4可以隨意的進行重排序執(zhí)行胧砰,即指令執(zhí)行過程可能是3214或1324鳍鸵。
- 如果是volatile修飾的變量instance,那么會在指令3的前后各插入一個內存屏障尉间;指令3不會在指令1和指令2之前執(zhí)行偿乖,也不會在指令4和5之后執(zhí)行;但指令1和2哲嘲,指令3和4順序是不固定的贪薪;
- 并且能保證,執(zhí)行到指令3時撤蚊,指令1和指令2必定是執(zhí)行完畢了的古掏,且指令1和指令2的執(zhí)行結果對指令3、指令4侦啸、指令5是可見的槽唾。
4. 如何保證可見性?
以下內容來源于:深入分析Volatile的實現原理-方騰飛-并發(fā)編程網:
通過觀察volatile變量和普通變量所生成的匯編代碼可以發(fā)現光涂,操作volatile變量會多出一個lock前綴指令:
//instance是volatile變量
instance = new Singleton();
// 匯編代碼
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);
有volatile變量修飾的共享變量進行寫操作的時候會多第二行匯編代碼庞萍,通過查IA-32架構軟件開發(fā)者手冊可知,lock前綴的指令在多核處理器下會引發(fā)了兩件事情忘闻。
- 將當前處理器緩存行的數據會寫回到系統內存钝计;
- 這個寫回內存的操作會引起在其他CPU里緩存了該內存地址的數據無效;
處理器為了提高處理速度,不直接和內存進行通訊私恬,而是先將系統內存的數據讀到內部緩存(L1,L2或其他)后再進行操作债沮,但操作完之后不知道何時會寫到內存,如果對聲明了Volatile變量進行寫操作本鸣,JVM就會向處理器發(fā)送一條Lock前綴的指令疫衩,將這個變量所在緩存行的數據寫回到系統內存。但是就算寫回到內存荣德,如果其他處理器緩存的值還是舊的闷煤,再執(zhí)行計算操作就會有問題,所以在多處理器下涮瞻,為了保證各個處理器的緩存是一致的鲤拿,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了署咽,當處理器發(fā)現自己緩存行對應的內存地址被修改近顷,就會將當前處理器的緩存行設置成無效狀態(tài),當處理器要對這個數據進行修改操作的時候宁否,會強制重新從系統內存里把數據讀到處理器緩存里幕庐。
所以,如果一個變量被volatile所修飾的話家淤,在每次數據變化之后,其值都會被強制刷入主存瑟由。而其他處理器的緩存由于遵守了緩存一致性協議絮重,也會把這個變量的值從主存加載到自己的緩存中。這就保證了一個volatile在并發(fā)編程中歹苦,其值在多個緩存中是可見的青伤。
5. 單例雙重檢查問題
雙重檢查鎖定(DCL)是單例模式的一種實現方式,實現代碼大致如下:
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
很多人會忽略volatile關鍵字殴瘦,因為沒有該關鍵字狠角,程序一般情況下也可以很好的運行。不過由于指令重排蚪腋,可能會導致instance已經指向了內存地址丰歌,也就是已經不為null,但構造器代碼還沒有執(zhí)行屉凯,而使用volatile 則可以解決這個問題立帖。而有關這塊更多的介紹,可以參考:雙重檢查鎖定(DCL)失效與volatile
不過根據《Java并發(fā)編程實戰(zhàn)》的說法悠砚,這種雙重檢查加鎖正在廣泛的被廢棄掉晓勇,如果僅對單例而言的話,我們肯定始有更好的實現方式的。
三绑咱、總結
簡單介紹了volatile關鍵字的用法绰筛,這里來簡單總結下:
- 使用volatile關鍵字可以保證共享變量的可見性,并且可以禁止指令重排序描融,但volatile關鍵字不能保證原子性铝噩;;
- 和加鎖機制相比稼稿,加鎖機制既可以確北¢唬可見性又可以確保原子性,而volatile變量只能確比眉撸可見性敞恋;
- volatile變量雖然很方便,但通常用作某個操作完成谋右、發(fā)生中斷或者狀態(tài)的標志硬猫,官方并不建議過度依賴volatile變量提供的可見性;僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時改执,才應該使用他們啸蜜;如果在驗證正確性時需要對可見性進行復雜的判斷,那么就不要使用volatile變量辈挂。
本文參考自:
《Java并發(fā)編程實戰(zhàn)》《深入理解Java虛擬機》
并發(fā)編程網-深入分析Volatile的實現原理
海子-Java并發(fā)編程-volatile關鍵字解析
java內存模型介紹-hollischuang.com