volatile 是并發(fā)編程的重要組成部分粗卜,也是面試常被問(wèn)到的問(wèn)題之一。不要向小強(qiáng)那樣纳击,因?yàn)橐痪洌簐olatile 是輕量級(jí)的 synchronized续扔,而與期望已久的大廠失之交臂。
volatile 有兩大特性:保證內(nèi)存的可見(jiàn)性和禁止指令重排序焕数。那什么是可見(jiàn)性和指令重排呢纱昧?接下來(lái)我們一起來(lái)看。
內(nèi)存可見(jiàn)性
要了解內(nèi)存可見(jiàn)性先要從 Java 內(nèi)存模型(JMM)說(shuō)起堡赔,在 Java 中所有的共享變量都在主內(nèi)存中识脆,每個(gè)線程都有自己的工作內(nèi)存,為了提高線程的運(yùn)行速度,每個(gè)線程的工作內(nèi)存都會(huì)把主內(nèi)存中的共享變量拷貝一份進(jìn)行緩存灼捂,以此來(lái)提高運(yùn)行效率离例,內(nèi)存布局如下圖所示:
![內(nèi)存可見(jiàn)性.png](https://upload-images.jianshu.io/upload_images/2146214-3c4ad53d58dc4fbd.png&originHeight=511&originWidth=777&size=44249&status=done&style=none&width=777?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
但這樣就會(huì)產(chǎn)生一個(gè)新的問(wèn)題,如果某個(gè)線程修改了共享變量的值悉稠,其他線程不知道此值被修改了宫蛆,就會(huì)發(fā)生兩個(gè)線程值不一致的情況,我們用代碼來(lái)演示一下這個(gè)問(wèn)題的猛。
public class VolatileExample {
// 可見(jiàn)性參數(shù)
private static boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
try {
// 暫停 0.5s 執(zhí)行
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag 被修改成 true");
}).start();
// 一直循環(huán)檢測(cè) flag=true
while (true) {
if (flag) {
System.out.println("檢測(cè)到 flag 變?yōu)?true");
break;
}
}
}
}
以上程序的執(zhí)行結(jié)果如下:
flag 被修改成 true
我們會(huì)發(fā)現(xiàn)永遠(yuǎn)等不到 檢測(cè)到 flag 變?yōu)?true
的結(jié)果耀盗,這是因?yàn)榉侵骶€程更改了 flag=true,但主線程一直不知道此值發(fā)生了改變衰絮,這就是內(nèi)存不可見(jiàn)的問(wèn)題袍冷。
內(nèi)存的可見(jiàn)性是指線程修改了變量的值之后,其他線程能立即知道此值發(fā)生了改變猫牡。
我們可以使用 volatile 來(lái)修飾 flag胡诗,就可以保證內(nèi)存的可見(jiàn)性,代碼如下:
public class VolatileExample {
// 可見(jiàn)性參數(shù)
private static volatile boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
try {
// 暫停 0.5s 執(zhí)行
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag 被修改成 true");
}).start();
// 一直循環(huán)檢測(cè) flag=true
while (true) {
if (flag) {
System.out.println("檢測(cè)到 flag 變?yōu)?true");
break;
}
}
}
}
以上程序的執(zhí)行結(jié)果如下:
檢測(cè)到 flag 變?yōu)?true
flag 被修改成 true
指令重排
指令重排是指在執(zhí)行程序時(shí)淌友,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序煌恢,已到達(dá)提高程序性能的目的。
比如小強(qiáng)要去圖書(shū)館還上次借的書(shū)震庭,隨便再借一本新書(shū)瑰抵,而此時(shí)室友小王也想讓小強(qiáng)幫他還一本書(shū),未發(fā)生指令重排的做法是器联,小強(qiáng)先把自己的事情辦完二汛,再去辦室友的事,這樣顯然比較浪費(fèi)時(shí)間拨拓,還有一種做法是肴颊,他先把自己的書(shū)和小王的書(shū)一起還掉,再給自己借一本新書(shū)渣磷,這就是指令重排的意義婿着。
但指令重排不能保證指令執(zhí)行的順序,這就會(huì)造成新的問(wèn)題醋界,如下代碼所示:
public class VolatileExample {
// 指令重排參數(shù)
private static int a = 0, b = 0;
private static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
Thread t1 = new Thread(() -> {
// 有可能發(fā)生指令重排竟宋,先 x=b 再 a=1
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
// 有可能發(fā)生指令重排,先 y=a 再 b=1
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("第 " + i + "次形纺,x=" + x + " | y=" + y);
if (x == 0 && y == 0) {
// 發(fā)生了指令重排
break;
}
// 初始化變量
a = 0;
b = 0;
x = 0;
y = 0;
}
}
}
以上程序執(zhí)行結(jié)果如下所示:
![指令重排.png](https://upload-images.jianshu.io/upload_images/2146214-d19abd7556fd8bf2.png&originHeight=336&originWidth=372&size=17559&status=done&style=none&width=372?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以看出執(zhí)行到 48526 次時(shí)發(fā)生了指令重排丘侠,y 就變成了非正確值 0,顯然這不是我們想要的結(jié)果挡篓,這個(gè)時(shí)候就可以使用 volatile 來(lái)禁止指令重排婉陷。
以上我們通過(guò)代碼的方式演示了指令重排和內(nèi)存可見(jiàn)性的問(wèn)題帚称,接下來(lái)我們用代碼來(lái)演示一下 volatile 同步方式的問(wèn)題。
volatile 非同步方式
首先秽澳,我們使用 volatile 修飾一個(gè)整數(shù)變量闯睹,再啟動(dòng)兩個(gè)線程分別執(zhí)行同樣次數(shù)的 ++ 和 -- 操作,最后發(fā)現(xiàn)執(zhí)行的結(jié)果竟然不是 0担神,代碼如下:
public class VolatileExample {
public static volatile int count = 0; // 計(jì)數(shù)器
public static final int size = 100000; // 循環(huán)測(cè)試次數(shù)
public static void main(String[] args) {
// ++ 方式
Thread thread = new Thread(() -> {
for (int i = 1; i <= size; i++) {
count++;
}
});
thread.start();
// -- 方式
for (int i = 1; i <= size; i++) {
count--;
}
// 等所有線程執(zhí)行完成
while (thread.isAlive()) {}
System.out.println(count); // 打印結(jié)果
}
}
以上程序執(zhí)行結(jié)果如下:
1065
可以看出楼吃,執(zhí)行結(jié)果并不是我們期望的結(jié)果 0,我們把以上代碼使用 synchronized 改造一下:
public class VolatileExample {
public static int count = 0; // 計(jì)數(shù)器
public static final int size = 100000; // 循環(huán)測(cè)試次數(shù)
public static void main(String[] args) {
// ++ 方式
Thread thread = new Thread(() -> {
for (int i = 1; i <= size; i++) {
synchronized (VolatileExample.class) {
count++;
}
}
});
thread.start();
// -- 方式
for (int i = 1; i <= size; i++) {
synchronized (VolatileExample.class) {
count--;
}
}
// 等所有線程執(zhí)行完成
while (thread.isAlive()) {}
System.out.println(count); // 打印結(jié)果
}
}
這次執(zhí)行的結(jié)果變成了我們期望的值 0妄讯。
這說(shuō)明 volatile 只是輕量級(jí)的線程可見(jiàn)方式孩锡,并不是輕量級(jí)的同步方式,所以并不能說(shuō) volatile 是輕量級(jí)的 synchronized亥贸,終于知道為什么面試官讓我回去等通知了躬窜。
volatile 使用場(chǎng)景
既然 volatile 只能保證線程操作的可見(jiàn)方式,那它有什么用呢炕置?
volatile 在多讀多寫(xiě)的情況下雖然一定會(huì)有問(wèn)題荣挨,但如果是一寫(xiě)多讀的話使用 volatile 就不會(huì)有任何問(wèn)題。volatile 一寫(xiě)多讀的經(jīng)典使用示例就是 CopyOnWriteArrayList朴摊,CopyOnWriteArrayList 在操作的時(shí)候會(huì)把全部數(shù)據(jù)復(fù)制出來(lái)對(duì)寫(xiě)操作加鎖默垄,修改完之后再使用 setArray 方法把此數(shù)組賦值為更新后的值,使用 volatile 可以使讀線程很快的告知到數(shù)組被修改甚纲,不會(huì)進(jìn)行指令重排口锭,操作完成后就可以對(duì)其他線程可見(jiàn)了,核心源碼如下:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private transient volatile Object[] array;
final void setArray(Object[] a) {
array = a;
}
//...... 忽略其他代碼
}
總結(jié)
本文我們通過(guò)代碼的方式演示了 volatile 的兩大特性介杆,內(nèi)存可見(jiàn)性和禁止指令重排鹃操,使用 ++ 和 -- 的方式演示了 volatile 并非輕量級(jí)的同步方式,以及 volatile 一寫(xiě)多讀的經(jīng)典使用案例 CopyOnWriteArrayList春哨。
更多 Java 原創(chuàng)文章组民,請(qǐng)關(guān)注我微信公眾號(hào) 「Java中文社群」