1. 基本概念
volatile 關鍵字,具有兩個特性:1. 內(nèi)存的可見性, 2. 禁止指令重排序優(yōu)化。
內(nèi)存可見性
被 volatile 關鍵字修飾的變量,當線程要對這個變量執(zhí)行的寫操作,都不會寫入本地緩存,而是直接刷入主內(nèi)存中。當線程讀取被 volatile 關鍵字修飾的變量時,也是直接從主內(nèi)存中讀取。
注意:volatile 不能保證原子性。很多時候都會誤用。
下面是問題代碼:
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 關鍵字就能夠?qū)崿F(xiàn)對 int 變量的原子操作,事實并非這樣。上面代碼每次運行的結(jié)果都不相同。看望上面這些基本概念,下面就開始深入理解下 volatile 這個關鍵字吧。
2. JVM 內(nèi)存模型
2.1 可見性
可見性,是指線程之間的可見性,一個線程修改的狀態(tài)對另一個線程是可見的。也就是一個線程修改的結(jié)果。另一個線程馬上就能看到。比如:用 volatile 修飾的變量,就會具有可見性。volatile 修飾的變量不允許線程內(nèi)部緩存和重排序,即直接修改內(nèi)存。所以對其他線程是可見的。但是這里需要注意一個問題,volatile 只能讓被他修飾內(nèi)容具有可見性,但不能保證它具有原子性。
2.2 原子性
原子是世界上的最小單位,具有不可分割性。比如 a=0;(a 非 long 和 double 類型) 這個操作是不可分割的,那么我們說這個操作時原子操作。再比如:a++; 這個操作實際是 a = a + 1;是可分割的,所以他不是一個原子操作。java 的 concurrent 包下提供了一些原子類,我們可以通過閱讀 API 來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
2.3 有序性
Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行執(zhí)行。
3. volatile 原理
當一個變量定義為 volatile 之后,將具備兩種特性:
- 保證此變量對所有的線程的可見性,這里的“可見性”,如本文開頭所述,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成。(隨著虛擬機的優(yōu)化,普通變量也可以具有可見性了,下面來看一個代碼)
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();
// 等一段時間,目的是為了能夠讓線程啟動并進入到 run 方法里
TimeUnit.MILLISECONDS.sleep(300);
flag = true;
}
}
上面這段代碼永遠不會結(jié)束,因為對 flag 的修改是在 main 線程的本地工作內(nèi)存中的,flag 的值對其他線程不可見。對 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();
// 等一段時間,目的是為了能夠讓線程啟動并進入到 run 方法里
TimeUnit.MILLISECONDS.sleep(300);
flag = true;
}
}
但是對上面這段代碼在稍作修改,發(fā)現(xiàn)其實也可以不用 volatile 關鍵字,普通變量照樣能夠?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();
// 等一段時間,目的是為了能夠讓線程啟動并進入到 run 方法里
TimeUnit.MILLISECONDS.sleep(300);
flag = true;
}
}
這是什么原因呢?原來只有在對變量讀取頻率很高的情況下,虛擬機才不會及時回寫主內(nèi)存,而當頻率沒有達到虛擬機認為的高頻率時,普通變量和volatile是同樣的處理邏輯。如在每個循環(huán)中執(zhí)行System.out.println(1)加大了讀取變量的時間間隔,使虛擬機認為讀取頻率并不那么高,所以實現(xiàn)了和volatile的效果。
- 禁止指令重排序優(yōu)化。有volatile修飾的變量,賦值后多執(zhí)行了一個 “l(fā)oad addl $0x0, (%esp)” 操作,這個操作相當于一個內(nèi)存屏障(指令重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個 CPU 訪問內(nèi)存時,并不需要內(nèi)存屏障
4. 深入理解指令重排序和內(nèi)存屏障
4.1 as-if-serial 語義
as-if-serial 的語義是:不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守“as-if-serial”語義。
為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果。
但是,如果操作之間不存在數(shù)據(jù)依賴關系,這些操作就可能被編譯器和處理器重排序。
拿個簡單例子來說:
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 語義把單線程程序保護了起來,遵守 as-if-serial 語義的編譯器、runtime 和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的。as-if-serial 語義使單線程程序員無需擔心重排序會干擾他們,也無需擔心內(nèi)存可見性問題。
4.2 指令重排序(happens-before)
重排序的規(guī)則
★1. 程序次序規(guī)則(Program Order Rule):在一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準確地說應該是控制流順序而不是代碼順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。
★2. 監(jiān)視器鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個對象鎖的lock操作。這里強調(diào)的是同一個鎖,而“后面”指的是時間上的先后順序,如發(fā)生在其他線程中的lock操作。
★3. volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作發(fā)生于后面對這個變量的讀操作,這里的“后面”也指的是時間上的先后順序。
線程啟動規(guī)則(Thread Start Rule):Thread獨享的start()方法先行于此線程的每一個動作。
線程終止規(guī)則(Thread Termination Rule):線程中的每個操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值檢測到線程已經(jīng)終止執(zhí)行。
線程中斷規(guī)則(Thread Interruption Rule):對線程interrupte()方法的調(diào)用優(yōu)先于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否已中斷。
對象終結(jié)原則(Finalizer Rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。
★8. 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
如果我們的多線程程序依賴于代碼書寫順序,那么就要考慮是否符合以上規(guī)則,如果不符合就要通過一些機制使其符合,最常用的就是synchronized、Lock以及volatile修飾符。
值得注意的是:兩個操作之間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執(zhí)行! happens-before 僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見,且前一個操作按順序排在第二個操作之前。
舉一個例子來理解重排序,看下面的代碼:
public class SimpleHappenBefore {
/** 這是一個驗證結(jié)果的變量 */
private static int a=0;
/** 這是一個標志位 */
private static boolean flag=false;
public static void main(String[] args) throws InterruptedException {
//由于多線程情況下未必會試出重排序的結(jié)論,所以多試一些次
for(int i=0;i<1000;i++){
ThreadA threadA=new ThreadA();
ThreadB threadB=new ThreadB();
threadA.start();
threadB.start();
//這里等待線程結(jié)束后,重置共享變量,以使驗證結(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 變量是個標記,用來標識變量 a 是否已被寫入。這里假設有兩個線程 A 和 B,A 首先執(zhí)行 writer() 方法,隨后 B 線程接著執(zhí)行 reader() 方法。線程 B 在執(zhí)行操作 4 時,能否看到線程 A 在操作 1 對共享變量 a 的寫入?
答案是:不一定能看到。
由于操作1和操作2沒有數(shù)據(jù)依賴關系,編譯器和處理器可以對這兩個操作重排序;同樣,操作3和操作4沒有數(shù)據(jù)依賴關系,編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看,當操作1和操作2重排序時,可能會產(chǎn)生什么效果?請看下面的程序執(zhí)行時序圖:
如上圖所示,操作1和操作2做了重排序。程序執(zhí)行時,線程A首先寫標記變量flag,隨后線程B讀這個變量。由于條件判斷為真,線程B將讀取變量a。此時,變量a還根本沒有被線程A寫入,在這里多線程程序的語義被重排序破壞了!
※注:本文統(tǒng)一用紅色的虛箭線表示錯誤的讀操作,用綠色的虛箭線表示正確的讀操作。
下面再讓我們看看,當操作3和操作4重排序時會產(chǎn)生什么效果(借助這個重排序,可以順便說明控制依賴性)。下面是操作3和操作4重排序后,程序的執(zhí)行時序圖:
在程序中,操作3和操作4存在控制依賴關系。當代碼中存在控制依賴性時,會影響指令序列執(zhí)行的并行度。為此,編譯器和處理器會采用猜測(Speculation)執(zhí)行來克服控制相關性對并行度的影響。以處理器的猜測執(zhí)行為例,執(zhí)行線程B的處理器可以提前讀取并計算a*a,然后把計算結(jié)果臨時保存到一個名為重排序緩沖(reorder buffer ROB)的硬件緩存中。當接下來操作3的條件判斷為真時,就把該計算結(jié)果寫入變量i中。
從圖中我們可以看出,猜測執(zhí)行實質(zhì)上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義!
除此之外,Java內(nèi)存模型對volatile和final的語義做了擴展。對volatile語義的擴展保證了volatile變量在一些情況下不會重排序,volatile的64位變量double和long的讀取和賦值操作都是原子的。對final語義的擴展保證一個對象的構(gòu)建方法結(jié)束前,所有final成員變量都必須完成初始化(的前提是沒有this引用溢出)。
Java內(nèi)存模型關于重排序的規(guī)定,總結(jié)后如下表所示。

表中“第二項操作”的含義是指,第一項操作之后的所有指定操作。如,普通讀不能與其之后的所有volatile寫重排序。另外,JMM也規(guī)定了上述volatile和同步塊的規(guī)則盡適用于存在多線程訪問的情景。例如,若編譯器(這里的編譯器也包括JIT,下同)證明了一個volatile變量只能被單線程訪問,那么就可能會把它做為普通變量來處理。
留白的單元格代表允許在不違反Java基本語義的情況下重排序。例如,編譯器不會對對同一內(nèi)存地址的讀和寫操作重排序,但是允許對不同地址的讀和寫操作重排序。
除此之外,為了保證final的新增語義。JSR-133對于final變量的重排序也做了限制。
-
構(gòu)建方法內(nèi)部的final成員變量的存儲,并且,假如final成員變量本身是一個引用的話,這個final成員變量可以引用到的一切存儲操作,都不能與構(gòu)建方法外的將當期構(gòu)建對象賦值于多線程共享變量的存儲操作重排序。
例如對于如下語句
x.finalField = v; ... ;構(gòu)建方法邊界sharedRef = x;v.afield = 1; x.finalField = v; ... ; 構(gòu)建方法邊界sharedRef = x;
這兩條語句中,構(gòu)建方法邊界前后的指令都不能重排序。
-
初始讀取共享對象與初始讀取該共享對象的final成員變量之間不能重排序。
例如對于如下語句
x = sharedRef; ... ; i = x.finalField;
前后兩句語句之間不會發(fā)生重排序。由于這兩句語句有數(shù)據(jù)依賴關系,編譯器本身就不會對它們重排序,但確實有一些處理器會對這種情況重排序,因此特別制定了這一規(guī)則。
4.3 內(nèi)存屏障
內(nèi)存屏障(Memory Barrier,或有時叫做內(nèi)存柵欄,Memory Fence)是一種CPU指令,用于控制特定條件下的重排序和內(nèi)存可見性問題。Java編譯器也會根據(jù)內(nèi)存屏障的規(guī)則禁止重排序。
內(nèi)存屏障可以被分為以下幾種類型
LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。 在大多數(shù)處理器的實現(xiàn)中,這個屏障是個萬能屏障,兼具其它三種內(nèi)存屏障的功能。
有的處理器的重排序規(guī)則較嚴,無需內(nèi)存屏障也能很好的工作,Java編譯器會在這種情況下不放置內(nèi)存屏障。
為了實現(xiàn)上一章中討論的JSR-133的規(guī)定,Java編譯器會這樣使用內(nèi)存屏障。

為了保證final字段的特殊語義,也會在下面的語句加入內(nèi)存屏障。
x.finalField = v; StoreStore; sharedRef = x;