1. 基本概念
volatile 關(guān)鍵字,具有兩個(gè)特性:1. 內(nèi)存的可見性, 2. 禁止指令重排序優(yōu)化。
內(nèi)存可見性
被 volatile 關(guān)鍵字修飾的變量,當(dāng)線程要對(duì)這個(gè)變量執(zhí)行的寫操作,都不會(huì)寫入本地緩存枢步,而是直接刷入主內(nèi)存中。當(dāng)線程讀取被 volatile 關(guān)鍵字修飾的變量時(shí)渐尿,也是直接從主內(nèi)存中讀取醉途。
注意:volatile 不能保證原子性。很多時(shí)候都會(huì)誤用砖茸。
下面是問題代碼:
public class VolatileDemo {
static volatile int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
VolatileDemo.inc();
}
}).start();
}
System.out.println(VolatileDemo.count);
}
static void inc() {
count++;
}
}
很多人以為加上了 volatile 關(guān)鍵字就能夠?qū)崿F(xiàn)對(duì) int 變量的原子操作隘擎,事實(shí)并非這樣。上面代碼每次運(yùn)行的結(jié)果都不相同凉夯∏妒海看望上面這些基本概念推正,下面就開始深入理解下 volatile 這個(gè)關(guān)鍵字吧恍涂。
2. JVM 內(nèi)存模型
2.1 可見性
可見性宝惰,是指線程之間的可見性,一個(gè)線程修改的狀態(tài)對(duì)另一個(gè)線程是可見的再沧。也就是一個(gè)線程修改的結(jié)果尼夺。另一個(gè)線程馬上就能看到。比如:用 volatile 修飾的變量炒瘸,就會(huì)具有可見性淤堵。volatile 修飾的變量不允許線程內(nèi)部緩存和重排序,即直接修改內(nèi)存顷扩。所以對(duì)其他線程是可見的拐邪。但是這里需要注意一個(gè)問題,volatile 只能讓被他修飾內(nèi)容具有可見性隘截,但不能保證它具有原子性扎阶。
2.2 原子性
原子是世界上的最小單位,具有不可分割性婶芭。比如 a=0东臀;(a 非 long 和 double 類型) 這個(gè)操作是不可分割的,那么我們說這個(gè)操作時(shí)原子操作犀农。再比如:a++惰赋; 這個(gè)操作實(shí)際是 a = a + 1;是可分割的呵哨,所以他不是一個(gè)原子操作赁濒。java 的 concurrent 包下提供了一些原子類,我們可以通過閱讀 API 來了解這些原子類的用法孟害。比如:AtomicInteger拒炎、AtomicLong、AtomicReference等纹坐。
2.3 有序性
Java 語言提供了 volatile 和 synchronized 兩個(gè)關(guān)鍵字來保證線程之間操作的有序性枝冀,volatile 是因?yàn)槠浔旧戆敖怪噶钪嘏判颉钡恼Z義,synchronized 是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作”這條規(guī)則獲得的耘子,此規(guī)則決定了持有同一個(gè)對(duì)象鎖的兩個(gè)同步塊只能串行執(zhí)行果漾。
3. volatile 原理
當(dāng)一個(gè)變量定義為 volatile 之后,將具備兩種特性:
- 保證此變量對(duì)所有的線程的可見性谷誓,這里的“可見性”绒障,如本文開頭所述,當(dāng)一個(gè)線程修改了這個(gè)變量的值捍歪,volatile 保證了新值能立即同步到主內(nèi)存户辱,以及每次使用前立即從主內(nèi)存刷新鸵钝。但普通變量做不到這點(diǎn),普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成庐镐。(隨著虛擬機(jī)的優(yōu)化恩商,普通變量也可以具有可見性了,下面來看一個(gè)代碼)
public class VolatileDemo extends Thread {
static boolean flag = false;
@Override
public void run() {
while (!flag) {}
}
public static void main(String[] args) throws InterruptedException {
new VolatileDemo().start();
// 等一段時(shí)間必逆,目的是為了能夠讓線程啟動(dòng)并進(jìn)入到 run 方法里
TimeUnit.MILLISECONDS.sleep(300);
flag = true;
}
}
上面這段代碼永遠(yuǎn)不會(huì)結(jié)束怠堪,因?yàn)閷?duì) flag 的修改是在 main 線程的本地工作內(nèi)存中的,flag 的值對(duì)其他線程不可見名眉。對(duì) flag 加上 volatile 修飾符在做測試粟矿,程序能夠正常結(jié)束退出。
public class VolatileDemo extends Thread {
static volatile boolean flag = false;
@Override
public void run() {
while (!flag) {}
}
public static void main(String[] args) throws InterruptedException {
new VolatileDemo().start();
// 等一段時(shí)間损拢,目的是為了能夠讓線程啟動(dòng)并進(jìn)入到 run 方法里
TimeUnit.MILLISECONDS.sleep(300);
flag = true;
}
}
但是對(duì)上面這段代碼在稍作修改陌粹,發(fā)現(xiàn)其實(shí)也可以不用 volatile 關(guān)鍵字,普通變量照樣能夠?qū)崿F(xiàn)內(nèi)存可見性福压,程序也能夠正常退出掏秩。代碼如下:
public class VolatileDemo extends Thread {
static boolean flag = false;
@Override
public void run() {
while (!flag) { System.out.println(1); }
}
public static void main(String[] args) throws InterruptedException {
new VolatileDemo().start();
// 等一段時(shí)間,目的是為了能夠讓線程啟動(dòng)并進(jìn)入到 run 方法里
TimeUnit.MILLISECONDS.sleep(300);
flag = true;
}
}
這是什么原因呢隧膏?原來只有在對(duì)變量讀取頻率很高的情況下哗讥,虛擬機(jī)才不會(huì)及時(shí)回寫主內(nèi)存,而當(dāng)頻率沒有達(dá)到虛擬機(jī)認(rèn)為的高頻率時(shí)胞枕,普通變量和volatile是同樣的處理邏輯杆煞。如在每個(gè)循環(huán)中執(zhí)行System.out.println(1)加大了讀取變量的時(shí)間間隔,使虛擬機(jī)認(rèn)為讀取頻率并不那么高腐泻,所以實(shí)現(xiàn)了和volatile的效果决乎。
- 禁止指令重排序優(yōu)化。有volatile修飾的變量派桩,賦值后多執(zhí)行了一個(gè) “l(fā)oad addl $0x0, (%esp)” 操作构诚,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障(指令重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個(gè) CPU 訪問內(nèi)存時(shí)铆惑,并不需要內(nèi)存屏障
4. 深入理解指令重排序和內(nèi)存屏障
4.1 as-if-serial 語義
as-if-serial 的語義是:不管怎么重排序范嘱,單線程程序的執(zhí)行結(jié)果不能被改變。編譯器员魏、runtime和處理器都必須遵守“as-if-serial”語義丑蛤。
為了遵守as-if-serial語義,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序撕阎,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果受裹。
但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序棉饶。
拿個(gè)簡單例子來說:
public void execute(){
int a=0;
int b=1;
int c=a+b;
}
這里a=0,b=1兩句可以隨便排序厦章,不影響程序邏輯結(jié)果,但c=a+b這句必須在前兩句的后面執(zhí)行照藻。
as-if-serial 語義把單線程程序保護(hù)了起來袜啃,遵守 as-if-serial 語義的編譯器、runtime 和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個(gè)幻覺:單線程程序是按程序的順序來執(zhí)行的岩梳。as-if-serial 語義使單線程程序員無需擔(dān)心重排序會(huì)干擾他們囊骤,也無需擔(dān)心內(nèi)存可見性問題。
4.2 指令重排序(happens-before)
重排序的規(guī)則
★1. 程序次序規(guī)則(Program Order Rule):在一個(gè)線程內(nèi)冀值,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作宫屠。準(zhǔn)確地說應(yīng)該是控制流順序而不是代碼順序列疗,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu)浪蹂。
★2. 監(jiān)視器鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)對(duì)象鎖的lock操作抵栈。這里強(qiáng)調(diào)的是同一個(gè)鎖,而“后面”指的是時(shí)間上的先后順序坤次,如發(fā)生在其他線程中的lock操作古劲。
★3. volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫操作發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的“后面”也指的是時(shí)間上的先后順序缰猴。
線程啟動(dòng)規(guī)則(Thread Start Rule):Thread獨(dú)享的start()方法先行于此線程的每一個(gè)動(dòng)作产艾。
線程終止規(guī)則(Thread Termination Rule):線程中的每個(gè)操作都先行發(fā)生于對(duì)此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束滑绒、Thread.isAlive()的返回值檢測到線程已經(jīng)終止執(zhí)行闷堡。
線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程interrupte()方法的調(diào)用優(yōu)先于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否已中斷疑故。
對(duì)象終結(jié)原則(Finalizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始杠览。
★8. 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C纵势,那就可以得出操作A先行發(fā)生于操作C的結(jié)論踱阿。
如果我們的多線程程序依賴于代碼書寫順序钦铁,那么就要考慮是否符合以上規(guī)則,如果不符合就要通過一些機(jī)制使其符合育瓜,最常用的就是synchronized、Lock以及volatile修飾符躏仇。
值得注意的是:兩個(gè)操作之間具有happens-before關(guān)系腺办,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行糟描! happens-before 僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見,且前一個(gè)操作按順序排在第二個(gè)操作之前船响。
舉一個(gè)例子來理解重排序,看下面的代碼:
public class SimpleHappenBefore {
/** 這是一個(gè)驗(yàn)證結(jié)果的變量 */
private static int a=0;
/** 這是一個(gè)標(biāo)志位 */
private static boolean flag=false;
public static void main(String[] args) throws InterruptedException {
//由于多線程情況下未必會(huì)試出重排序的結(jié)論,所以多試一些次
for(int i=0;i<1000;i++){
ThreadA threadA=new ThreadA();
ThreadB threadB=new ThreadB();
threadA.start();
threadB.start();
//這里等待線程結(jié)束后,重置共享變量,以使驗(yàn)證結(jié)果的工作變得簡單些.
threadA.join();
threadB.join();
a=0;
flag=false;
}
}
static class ThreadA extends Thread{
public void run(){
a=1; //1
flag=true; //2
}
}
static class ThreadB extends Thread{
public void run(){
if(flag){ //3
a=a*1; //4
}
if(a==0){
System.out.println("ha,a==0");
}
}
}
}
flag 變量是個(gè)標(biāo)記见间,用來標(biāo)識(shí)變量 a 是否已被寫入。這里假設(shè)有兩個(gè)線程 A 和 B米诉,A 首先執(zhí)行 writer() 方法,隨后 B 線程接著執(zhí)行 reader() 方法史侣。線程 B 在執(zhí)行操作 4 時(shí)拴泌,能否看到線程 A 在操作 1 對(duì)共享變量 a 的寫入?
答案是:不一定能看到。
由于操作1和操作2沒有數(shù)據(jù)依賴關(guān)系惊橱,編譯器和處理器可以對(duì)這兩個(gè)操作重排序蚪腐;同樣,操作3和操作4沒有數(shù)據(jù)依賴關(guān)系税朴,編譯器和處理器也可以對(duì)這兩個(gè)操作重排序回季。讓我們先來看看,當(dāng)操作1和操作2重排序時(shí)掉房,可能會(huì)產(chǎn)生什么效果茧跋?請(qǐng)看下面的程序執(zhí)行時(shí)序圖:
如上圖所示,操作1和操作2做了重排序卓囚。程序執(zhí)行時(shí)瘾杭,線程A首先寫標(biāo)記變量flag,隨后線程B讀這個(gè)變量哪亿。由于條件判斷為真粥烁,線程B將讀取變量a。此時(shí)蝇棉,變量a還根本沒有被線程A寫入讨阻,在這里多線程程序的語義被重排序破壞了!
※注:本文統(tǒng)一用紅色的虛箭線表示錯(cuò)誤的讀操作篡殷,用綠色的虛箭線表示正確的讀操作钝吮。
下面再讓我們看看,當(dāng)操作3和操作4重排序時(shí)會(huì)產(chǎn)生什么效果(借助這個(gè)重排序,可以順便說明控制依賴性)奇瘦。下面是操作3和操作4重排序后棘催,程序的執(zhí)行時(shí)序圖:
在程序中,操作3和操作4存在控制依賴關(guān)系耳标。當(dāng)代碼中存在控制依賴性時(shí)醇坝,會(huì)影響指令序列執(zhí)行的并行度。為此次坡,編譯器和處理器會(huì)采用猜測(Speculation)執(zhí)行來克服控制相關(guān)性對(duì)并行度的影響呼猪。以處理器的猜測執(zhí)行為例,執(zhí)行線程B的處理器可以提前讀取并計(jì)算a*a砸琅,然后把計(jì)算結(jié)果臨時(shí)保存到一個(gè)名為重排序緩沖(reorder buffer ROB)的硬件緩存中宋距。當(dāng)接下來操作3的條件判斷為真時(shí),就把該計(jì)算結(jié)果寫入變量i中明棍。
從圖中我們可以看出,猜測執(zhí)行實(shí)質(zhì)上對(duì)操作3和4做了重排序。重排序在這里破壞了多線程程序的語義嘁傀!
除此之外细办,Java內(nèi)存模型對(duì)volatile和final的語義做了擴(kuò)展。對(duì)volatile語義的擴(kuò)展保證了volatile變量在一些情況下不會(huì)重排序岛啸,volatile的64位變量double和long的讀取和賦值操作都是原子的茴肥。對(duì)final語義的擴(kuò)展保證一個(gè)對(duì)象的構(gòu)建方法結(jié)束前瓤狐,所有final成員變量都必須完成初始化(的前提是沒有this引用溢出)。
Java內(nèi)存模型關(guān)于重排序的規(guī)定嗓节,總結(jié)后如下表所示皆警。
表中“第二項(xiàng)操作”的含義是指,第一項(xiàng)操作之后的所有指定操作绸罗。如从诲,普通讀不能與其之后的所有volatile寫重排序靡羡。另外略步,JMM也規(guī)定了上述volatile和同步塊的規(guī)則盡適用于存在多線程訪問的情景。例如绽诚,若編譯器(這里的編譯器也包括JIT恩够,下同)證明了一個(gè)volatile變量只能被單線程訪問蜂桶,那么就可能會(huì)把它做為普通變量來處理也切。
留白的單元格代表允許在不違反Java基本語義的情況下重排序雷恃。例如,編譯器不會(huì)對(duì)對(duì)同一內(nèi)存地址的讀和寫操作重排序倒槐,但是允許對(duì)不同地址的讀和寫操作重排序旬痹。
除此之外,為了保證final的新增語義导犹。JSR-133對(duì)于final變量的重排序也做了限制唱凯。
-
構(gòu)建方法內(nèi)部的final成員變量的存儲(chǔ),并且谎痢,假如final成員變量本身是一個(gè)引用的話磕昼,這個(gè)final成員變量可以引用到的一切存儲(chǔ)操作,都不能與構(gòu)建方法外的將當(dāng)期構(gòu)建對(duì)象賦值于多線程共享變量的存儲(chǔ)操作重排序节猿。
例如對(duì)于如下語句
x.finalField = v; ... ;構(gòu)建方法邊界sharedRef = x;v.afield = 1; x.finalField = v; ... ; 構(gòu)建方法邊界sharedRef = x;
這兩條語句中漫雕,構(gòu)建方法邊界前后的指令都不能重排序浸间。
-
初始讀取共享對(duì)象與初始讀取該共享對(duì)象的final成員變量之間不能重排序吟榴。
例如對(duì)于如下語句
x = sharedRef; ... ; i = x.finalField;
前后兩句語句之間不會(huì)發(fā)生重排序吩翻。由于這兩句語句有數(shù)據(jù)依賴關(guān)系,編譯器本身就不會(huì)對(duì)它們重排序细移,但確實(shí)有一些處理器會(huì)對(duì)這種情況重排序弧轧,因此特別制定了這一規(guī)則碗殷。
4.3 內(nèi)存屏障
內(nèi)存屏障(Memory Barrier锌妻,或有時(shí)叫做內(nèi)存柵欄,Memory Fence)是一種CPU指令,用于控制特定條件下的重排序和內(nèi)存可見性問題牍陌。Java編譯器也會(huì)根據(jù)內(nèi)存屏障的規(guī)則禁止重排序员咽。
內(nèi)存屏障可以被分為以下幾種類型
LoadLoad屏障:對(duì)于這樣的語句Load1; LoadLoad; Load2贝室,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前滑频,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreStore屏障:對(duì)于這樣的語句Store1; StoreStore; Store2银伟,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對(duì)其它處理器可見傅物。
LoadStore屏障:對(duì)于這樣的語句Load1; LoadStore; Store2董饰,在Store2及后續(xù)寫入操作被刷出前圆米,保證Load1要讀取的數(shù)據(jù)被讀取完畢榨咐。
StoreLoad屏障:對(duì)于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前齿坷,保證Store1的寫入對(duì)所有處理器可見永淌。它的開銷是四種屏障中最大的遂蛀。 在大多數(shù)處理器的實(shí)現(xiàn)中干厚,這個(gè)屏障是個(gè)萬能屏障,兼具其它三種內(nèi)存屏障的功能所坯。
有的處理器的重排序規(guī)則較嚴(yán)芹助,無需內(nèi)存屏障也能很好的工作状土,Java編譯器會(huì)在這種情況下不放置內(nèi)存屏障伺糠。
為了實(shí)現(xiàn)上一章中討論的JSR-133的規(guī)定退盯,Java編譯器會(huì)這樣使用內(nèi)存屏障泻肯。
為了保證final字段的特殊語義灶挟,也會(huì)在下面的語句加入內(nèi)存屏障稚铣。
x.finalField = v; StoreStore; sharedRef = x;