教妹學(xué)Java:并發(fā)編程之 volatile
二哥編程知識(shí)星球 (戳鏈接加入)正式上線了,來(lái)和 260 多名 小伙伴一起打怪升級(jí)吧!這是一個(gè) Java 學(xué)習(xí)指南 + 編程實(shí)戰(zhàn)的私密圈子,你可以向二哥提問(wèn)、幫你制定學(xué)習(xí)計(jì)劃、跟著二哥一起做實(shí)戰(zhàn)項(xiàng)目,沖沖沖。
Java 程序員進(jìn)階之路網(wǎng)址:https://tobebetterjavaer.com

“三妹啊,這節(jié)我們來(lái)學(xué)習(xí) Java 并發(fā)編程中的 volatile 關(guān)鍵字,以及容易遇到的坑。”看著三妹好學(xué)的樣子,我倍感欣慰。
“好呀,哥。”三妹愉快的答應(yīng)了。
volatile 變量的特性
volatile 可以保證可見(jiàn)性,但不保證原子性:
當(dāng)寫(xiě)一個(gè) volatile 變量時(shí),JMM 會(huì)把該線程本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去; 這個(gè)寫(xiě)操作會(huì)導(dǎo)致其他線程中的 volatile 變量緩存無(wú)效。
volatile 禁止指令重排規(guī)則
我們回顧一下,重排序需要遵守一定規(guī)則:
重排序操作不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。比如:a=1;b=a; 這個(gè)指令序列,由于第二個(gè)操作依賴于第一個(gè)操作,所以在編譯時(shí)和處理器運(yùn)行時(shí)這兩個(gè)操作不會(huì)被重排序。 重排序是為了優(yōu)化性能,但是不管怎么重排序,單線程下程序的執(zhí)行結(jié)果不能被改變。比如:a=1;b=2;c=a+b 這三個(gè)操作,第一步(a=1)和第二步(b=2)由于不存在數(shù)據(jù)依賴關(guān)系, 所以可能會(huì)發(fā)生重排序,但是 c=a+b 這個(gè)操作是不會(huì)被重排序的,因?yàn)樾枰WC最終的結(jié)果一定是 c=a+b=3。
使用 volatile 關(guān)鍵字修飾共享變量可以禁止這種重排序。若用 volatile 修飾共享變量,在編譯時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類型的處理器重排序,volatile 禁止指令重排序也有一些規(guī)則:
當(dāng)程序執(zhí)行到 volatile 變量的讀操作或者寫(xiě)操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見(jiàn);在其后面的操作肯定還沒(méi)有進(jìn)行; 在進(jìn)行指令優(yōu)化時(shí),不能將對(duì) volatile 變量訪問(wèn)的語(yǔ)句放在其后面執(zhí)行,也不能把 volatile 變量后面的語(yǔ)句放到其前面執(zhí)行。
“二哥,能不能通俗地講講啊?”
“也就是說(shuō),執(zhí)行到 volatile 變量時(shí),其前面的所有語(yǔ)句都執(zhí)行完,后面所有語(yǔ)句都未執(zhí)行。且前面語(yǔ)句的結(jié)果對(duì) volatile 變量及其后面語(yǔ)句可見(jiàn)。”我瞅了了三妹一眼說(shuō)。
volatile 禁止指令重排分析
先看下面未使用 volatile 的代碼:
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
System.out.println(i);
}
}
}
因?yàn)橹嘏判蛴绊懀宰罱K的輸出可能是 0,具體分析請(qǐng)參考上一篇,如果引入 volatile,我們?cè)倏匆幌麓a:
class ReorderExample {
int a = 0;
boolean volatile flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
System.out.println(i);
}
}
}
這個(gè)時(shí)候,volatile 禁止指令重排序也有一些規(guī)則,這個(gè)過(guò)程建立的 happens before 關(guān)系可以分為兩類:
根據(jù)程序次序規(guī)則,1 happens before 2; 3 happens before 4。 根據(jù) volatile 規(guī)則,2 happens before 3。 根據(jù) happens before 的傳遞性規(guī)則,1 happens before 4。
上述 happens before 關(guān)系的圖形化表現(xiàn)形式如下:

在上圖中,每一個(gè)箭頭鏈接的兩個(gè)節(jié)點(diǎn),代表了一個(gè) happens before 關(guān)系:
黑色箭頭表示程序順序規(guī)則; 橙色箭頭表示 volatile 規(guī)則; 藍(lán)色箭頭表示組合這些規(guī)則后提供的 happens before 保證。
這里 A 線程寫(xiě)一個(gè) volatile 變量后,B 線程讀同一個(gè) volatile 變量。A 線程在寫(xiě) volatile 變量之前所有可見(jiàn)的共享變量,在 B 線程讀同一個(gè) volatile 變量后,將立即變得對(duì) B 線程可見(jiàn)。
volatile 不適用場(chǎng)景
volatile 不適合復(fù)合操作
下面是變量自加的示例:
public class volatileTest {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final volatileTest test = new volatileTest();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println("inc output:" + test.inc);
}
}
測(cè)試輸出:
inc output:8182
“為什么呀?二哥?”三妹疑惑地問(wèn)。
“因?yàn)?inc++不是一個(gè)原子性操作,由讀取、加、賦值 3 步組成,所以結(jié)果并不能達(dá)到 10000。”我耐心地回答。
“哦,你這樣說(shuō)我就理解了。”三妹點(diǎn)點(diǎn)頭。
解決方法
采用 synchronized:
public class volatileTest1 {
public int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final volatileTest1 test = new volatileTest1();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println("add synchronized, inc output:" + test.inc);
}
}
采用 Lock:
public class volatileTest2 {
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
inc++;
lock.unlock();
}
public static void main(String[] args) {
final volatileTest2 test = new volatileTest2();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println("add lock, inc output:" + test.inc);
}
}
采用 AtomicInteger:
public class volatileTest3 {
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
public static void main(String[] args) {
final volatileTest3 test = new volatileTest3();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<100;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println("add AtomicInteger, inc output:" + test.inc);
}
}
三者輸出都是 1000,如下:
add synchronized, inc output:1000
add lock, inc output:1000
add AtomicInteger, inc output:1000
單例模式的雙重鎖要加volatile
先看一下單例代碼:
public class penguin {
private static volatile penguin m_penguin = null;
// 避免通過(guò)new初始化對(duì)象
private void penguin() {}
public void beating() {
System.out.println("打豆豆");
};
public static penguin getInstance() { //1
if (null == m_penguin) { //2
synchronized(penguin.class) { //3
if (null == m_penguin) { //4
m_penguin = new penguin(); //5
}
}
}
return m_penguin; //6
}
}
在并發(fā)情況下,如果沒(méi)有 volatile 關(guān)鍵字,在第 5 行會(huì)出現(xiàn)問(wèn)題。instance = new TestInstance();可以分解為 3 行偽代碼:
a. memory = allocate() //分配內(nèi)存
b. ctorInstanc(memory) //初始化對(duì)象
c. instance = memory //設(shè)置instance指向剛分配的地址
上面的代碼在編譯運(yùn)行時(shí),可能會(huì)出現(xiàn)重排序從 a-b-c 排序?yàn)?a-c-b。在多線程的情況下會(huì)出現(xiàn)以下問(wèn)題。
當(dāng)線程 A 在執(zhí)行第 5 行代碼時(shí),B 線程進(jìn)來(lái)執(zhí)行到第 2 行代碼。假設(shè)此時(shí) A 執(zhí)行的過(guò)程中發(fā)生了指令重排序,即先執(zhí)行了 a 和 c,沒(méi)有執(zhí)行 b。那么由于 A 線程執(zhí)行了 c 導(dǎo)致 instance 指向了一段地址,所以 B 線程判斷 instance 不為 null,會(huì)直接跳到第 6 行并返回一個(gè)未初始化的對(duì)象。
總結(jié)
“好了,三妹,我們來(lái)總結(jié)一下。”我舒了一口氣說(shuō)。
volatile 可以保證線程可見(jiàn)性且提供了一定的有序性,但是無(wú)法保證原子性。在 JVM 底層 volatile 是采用“內(nèi)存屏障”來(lái)實(shí)現(xiàn)的。
觀察加入 volatile 關(guān)鍵字和沒(méi)有加入 volatile 關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入 volatile 關(guān)鍵字時(shí),會(huì)多出一個(gè) lock 前綴指令,lock 前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也稱內(nèi)存柵欄),內(nèi)存屏障會(huì)提供 3 個(gè)功能:
它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成; 它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫(xiě)入主存; 如果是寫(xiě)操作,它會(huì)導(dǎo)致其他 CPU 中對(duì)應(yīng)的緩存行無(wú)效。
最后,我們學(xué)習(xí)了 volatile 不適用的場(chǎng)景,以及解決的方法,并解釋了單例模式為何需要使用 volatile。
沒(méi)有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧?kù)o的港灣,我是不系之舟。歡迎大家多多點(diǎn)贊,更多精彩內(nèi)容,也請(qǐng)關(guān)注二哥新成立的三劍客之一“樓仔”,我們也會(huì)做一些令大家驚喜的事情出來(lái),敬請(qǐng)期待!
推薦閱讀:
一鍵部署 Spring Boot 項(xiàng)目 離開(kāi)北京? 編程喵實(shí)戰(zhàn)項(xiàng)目可以在本地跑起來(lái)辣! 再見(jiàn) Spring Task,這款定時(shí)任務(wù)老而彌堅(jiān)!

