今天,來談談 Java 并發(fā)編程中的一個基礎知識點:volatile 關鍵字
本篇文章主要從可見性,原子性和有序性進行講解
- Java的內層模型
- happen-before
- 可見性
- 有序性
一. 主存與工作內存
說 volatile 之前,先來聊聊 Java 的內存模型。
在 Java 內存模型中,規(guī)定了所有的變量都是存儲在主內存當中,而每個線程都有屬于自己的工作內存。線程的工作內存保存了被該內存使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值等)都必須在工作內存中進行,而不能直接對主存進行操作。并且每個線程不能訪問其他線程的工作內存。
對于單線程的程序,這樣的規(guī)定沒有任何影響;但是對于多線程的程序,便可能導致,某個線程已經改變了主內存中的變量,而另一個線程還在使用其工作內存中的變量,因此造成了數(shù)據(jù)的不一致。

二. 可見性
volatile 可以保證數(shù)據(jù)的可見性,前面說到對于多線程的程序可能會造成數(shù)據(jù)不一致,但是當一個變量加上 volatile 之后,便可以保證,其他線程讀取到的該變量都是最新值。
這是因為每當對該變量進行寫操作時,都會使得其他線程工作變量中的該變量的拷貝失效,而迫使線程們都重新去主內存讀取
我們來看看實例:
public class TestThread extends Thread {
private volatile boolean isRunning = true;
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
int i = 1;
while (isRunning) {
i++;
}
System.out.println(i);
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
TestThread thread = new TestThread ();
thread.start();
Thread.sleep(3000);
thread.setRunning(false);
}
}
當 isRunning 變量沒有添加 volatile 變量時,該程序會發(fā)生死循環(huán),因為setRunning(false)并沒有影響到 thread 所在線程的工作內存(這時該線程看到的值仍然是 true)
當我們?yōu)樽兞刻砑由?volatile 之后,setRunning(false)執(zhí)行完畢,thread 所在線程的工作內存的變量拷貝便就此作廢,必須去主內存獲取最新的值,死循環(huán)也因此不會再發(fā)生了
值得注意的是,當我們在循環(huán)中添加了打印語句,或者 sleep 方法等,這時無論有沒有 volatile,都會停止循環(huán),如:
while (isRunning) {
i++;
System.out.println(i);
}
這是因為,JVM 會盡力保證內存的可見性,原本的代碼中,程序一直處于死循環(huán),這時 JVM 沒有辦法強制要求 CPU 分出時間去保證可見性;但是當加上打印語句之后,CPU 便會分出時間去處理這件事情,并保證了可見性;但是,與之不同的是,volatile 是強制保證可見性的。
三. 原子性
volatile 沒有辦法保證操作的原子性的
直接上代碼:
public class AtomicTest {
private static volatile int race = 0;
private static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increase();
}
});
threads[i].start();
}
//等待所有累加線程都結束
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(race);
}
}
這段代碼摘自《深入理解 Java 虛擬機》12 章,當 race 沒有 volatile 關鍵字的加持時,最終的打印結果經常會小于 10000,而有了 volatile,這段程序變不再出現(xiàn)這種情況。
假設兩個線程 1 和 2,它們倆先后讀取了 race 的值(初始值為 0),由于它們都還沒有進行寫操作,因此兩個線程這時看到的值都是 0,因此便使得之后兩次自增操作的結果是 1,而不是 2

剛剛說到 volatile 變量在進行寫操作的時候,會讓其他線程對應的工作內存中的拷貝失效,使得需要直接去主存中讀取變量,而上例中線程 1 在進行寫操作之前,線程 2 便已經執(zhí)行了讀操作,因此沒辦法影響線程 2 的讀取,因此也不會更新為最新的數(shù)據(jù)了
四. 有序性
volatile 可以在一定程度上禁止指令重排序
重排序
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段。在單線程程序中,對存在控制依賴的操作重排序,不會改變執(zhí)行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執(zhí)行結果。
//x、y為非volatile變量
//flag為volatile變量
x = 2; //語句1
y = 0; //語句2
flag = true; //語句3
x = 4; //語句4
y = -1; //語句5
flag 變量添加上 volatile 關鍵字以后,語句 1,2 不會排在 3 的后面執(zhí)行,當然 4,5 也不會在 3 的前面執(zhí)行
但是 1 和 2, 3 和 4 之間的順序沒辦法干預,這也是我們說“一定程度改變”的原因
上個例子:
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited){
sleep()
}
doSomethingwithconfig(context); //出錯,context 可能還沒有初始化
面對這樣的例子的時候,如果 inited 是非 volatile 變量,那么因為重排序的關系,有可能出錯;但是加上 volatile 后便不用擔心了
happens-before
如果一個操作執(zhí)行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。
對happens-before關系的具體定義如下。
① 如果一個操作happens-before另一個操作,那么第一個操作的執(zhí)行結果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
②兩個操作之間存在happens-before關系,并不意味著Java平臺的具體實現(xiàn)必須要按照 happens-before關系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結果,與按happens-before關系來執(zhí)行的結果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。
上面的①是JMM對程序員的承諾。從程序員的角度來說,可以這樣理解happens-before關系:如果A happens-before B,那么Java內存模型將向程序員保證——A操作的結果將對B可見,且A的執(zhí)行順序排在B之前。注意,這只是Java內存模型向程序員做出的保證!上面的②是JMM對編譯器和處理器重排序的約束原則。正如前面所言,其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。因此,happens-before關系本質上和as-if-serial語義是一回事。
as-if-serial語義保證單線程內程序的執(zhí)行結果不被改變,happens-before關系保證正確同步的多線程程序的執(zhí)行結果不被改變。
as-if-serial語義給編寫單線程程序的程序員創(chuàng)造了一個幻境:單線程程序是按程序的順序來執(zhí)行的。happens-before關系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執(zhí)行的。
as-if-serial語義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結果的前提下,盡可能地提高程序執(zhí)行的并行度。
happens-before規(guī)則如下:
程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。
傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
五. 使用場景
1. 狀態(tài)變量:
比如上面給出的可見性的代碼例子
while (isRunning) {
i++;
}
對于這種用于標記狀態(tài)的變量,volatile 是非常好用的
2. 雙重檢驗:
最經典的就是單例模式的雙重檢驗實現(xiàn),如果忘了的剛好復習一下:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
這里的 volatile,是為了保證 singleton = new Singleton();操作的有序性,因為 singleton = new Singleton(); 并不是原子操作,做了 3 件事
- 給 singleton 分配內存
- 調用 Singleton 的構造函數(shù)來初始化成員變量
- 將 singleton 對象指向分配的內存空間(執(zhí)行完這步 singleton 就為非 null 了)
但是由于重排序的原因,1-2-3 的順序可能變成 1-3-2,如果是后者,在 singleton 變成非 null 時(即第三步),如果第二個線程開始進入第一個判斷 if (singleton == null),那么便會直接返回 true,然而事實上 singleton 還沒有完成初始化