volatile
是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制
具有三大特性:
- 保證可見性
- 不保證原子性
- 禁止指令重排
要想講清楚這三大特性逾滥,首先要了解JMM
JMM
JMM(Java內(nèi)存模型 Java Memory Model)是一種抽象概念雄人,描述的是一組規(guī)則或規(guī)范降盹,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段谦纱,靜態(tài)字段和構(gòu)成數(shù)組的對(duì)象)的訪問方式
JMM關(guān)于同步的規(guī)定:
- 線程解鎖前孤澎,必須把共享變量的值刷新回主內(nèi)存
- 線程加鎖前新锈,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存
- 加鎖解鎖是同一把鎖
JVM會(huì)為每個(gè)線程開辟獨(dú)立的工作內(nèi)存(或稱為椇觊牛空間)保存線程私有數(shù)據(jù)捐祠,所有變量都保存在主內(nèi)存中碱鳞,線程對(duì)變量的操作必須在工作內(nèi)存中完成,首先將變量從主內(nèi)存拷貝至工作內(nèi)存踱蛀,然后對(duì)變量進(jìn)行操作窿给,完成后再將變量寫回主內(nèi)存,線程間無法訪問對(duì)方的工作內(nèi)存率拒,線程間通信必須通過主內(nèi)存來完成
JMM的三大特性:
- 可見性
- 原子性
- 有序性
volatile
滿足JMM三大特性的兩點(diǎn)
可見性
例:
public class VolatileDemo {
public static void main(String[] args) {
visibility();
}
private static void visibility() {
Data data = new Data();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " begin");
// 等待3秒后更新data.num
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.add(10);
System.out.println(Thread.currentThread().getName() + " updated num: " + data.num);
}, "thread-1").start();
// 主線程檢測(cè)當(dāng)data.num不為0時(shí)崩泡,結(jié)束循環(huán),否則一直等待
while (data.num == 0) {
}
System.out.println(Thread.currentThread().getName() + " is over, num: " + data.num);
}
}
class Data {
int num = 0;
public void add(int i) {
this.num += i;
}
}
結(jié)果:
thread-1 begin
thread-1 updated num: 10(main waiting)
主線程while循環(huán)沒有結(jié)束猬膨,而是一直循環(huán)角撞,可見某個(gè)線程對(duì)于共享變量的修改對(duì)于其他線程是不可見的,線程讀取到的數(shù)據(jù)副本不會(huì)因其他線程修改而改變
現(xiàn)在我們把num
變量添加volatile
關(guān)鍵字
public class VolatileDemo {
public static void main(String[] args) {
visibility();
}
private static void visibility() {
VolatileData data = new VolatileData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " begin");
// 等待3秒后更新data.num
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.add(10);
System.out.println(Thread.currentThread().getName() + " updated num: " + data.num);
}, "thread-1").start();
// 主線程檢測(cè)當(dāng)data.num不為0時(shí)勃痴,結(jié)束循環(huán)谒所,否則一直等待
while (data.num == 0) {
}
System.out.println(Thread.currentThread().getName() + " is over, num: " + data.num);
}
}
class VolatileData {
volatile int num = 0;
public void add(int i) {
this.num += i;
}
}
結(jié)果:
thread-1 begin
thread-1 updated num: 10
main is over, num: 10
主線程感知到了其他線程對(duì)于data.num
的修改,跳出循環(huán)執(zhí)行后面的語(yǔ)句沛申,并且主線程中data.num
的值與thread-1
線程中修改后的值一致劣领,所以說volatile
關(guān)鍵字保證了線程間共享變量的可見性
不保證原子性
原子性表示操作的完整性,當(dāng)某個(gè)線程正在對(duì)某數(shù)據(jù)進(jìn)行操作的過程中铁材,操作過程不可分割尖淘,只能操作成功或者操作失敗。
例:
public class VolatileDemo {
public static void main(String[] args) {
nonAtomic();
}
private static void nonAtomic() {
VolatileData data = new VolatileData();
// 通過20個(gè)線程著觉,每個(gè)線程執(zhí)行1000次自增操作村生,共20000次自增操作
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
data.increase();
}
}, "thread-" + i).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " final num: " + data.num);
}
}
class VolatileData {
volatile int num = 0;
public void add(int i) {
this.num += i;
}
public void increase() {
num++;
}
}
結(jié)果:
main final num: 19450
如果volatile
具有原子性,那么共執(zhí)行20000次的自增的結(jié)果應(yīng)該為20000饼丘,所以可見volatile
關(guān)鍵字不保證對(duì)變量操作的原子性
我們通過javap -c
反編譯字節(jié)碼文件的到increase()
方法指令如下:
public void increase();
Code:
0: aload_0
1: dup
2: getfield #2 // Field num:I
5: iconst_1
6: iadd
7: putfield #2 // Field num:I
10: return
我們可以看到num++
操作被轉(zhuǎn)化為了三個(gè)指令:
-
getfield
:獲取原始值 -
iadd
:執(zhí)行加1操作 -
putfield
:將修改后的值寫回
假設(shè)有兩個(gè)線程同時(shí)獲取到了原始值趁桃,線程1被掛起,線程2執(zhí)行自增并寫回,然后線程1執(zhí)行自增并寫回镇辉,由于兩個(gè)線程獲取到的原始值相同屡穗,所以兩個(gè)線程寫回的值也相同贴捡,這就導(dǎo)致了兩個(gè)線程的寫覆蓋忽肛,也就說明了操作不具有原子性
那么如何解決操作原子性問題呢?
- 對(duì)于
volatile
關(guān)鍵字修飾的變量操作添加synchronized
關(guān)鍵字 - 使用原子變量
AtomicInteger
例:
public class VolatileDemo {
public static void main(String[] args) {
nonAtomic();
}
private static void nonAtomic() {
VolatileData data = new VolatileData();
// 通過20個(gè)線程烂斋,每個(gè)線程執(zhí)行1000次自增操作屹逛,共20000次自增操作
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
data.increase();
data.atomicIncrease();
}
}, "thread-" + i).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " final num: " + data.num);
System.out.println(Thread.currentThread().getName() + " final atomic num: " + data.atomicNum);
}
}
class VolatileData {
volatile int num = 0;
AtomicInteger atomicNum = new AtomicInteger();
public void add(int i) {
this.num += i;
}
public void increase() {
this.num++;
}
public void atomicIncrease() {
this.atomicNum.getAndIncrement();
}
}
結(jié)果:
main final num: 19801
main final atomic num: 20000
可見多個(gè)線程對(duì)于原子變量的操作具有原子性
禁止指令重排
計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能汛骂,編譯器和處理器常常對(duì)指令進(jìn)行重新排序罕模,一般會(huì)進(jìn)行如下三步:
在單線程環(huán)境里面確保程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行結(jié)果一致
處理器在進(jìn)行重排序時(shí)必須要考慮指令之間的數(shù)據(jù)依賴性
多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在帘瞭,兩個(gè)線程中使用的變量能否保證一致性是無法確定的淑掌,結(jié)果無法預(yù)測(cè)
volatile
實(shí)現(xiàn)了禁止指令重排優(yōu)化,從而避免了多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象
首先了解一個(gè)概念蝶念,內(nèi)存屏障(Memory Barrier)也稱內(nèi)存柵欄抛腕,是一個(gè)CPU指令,它的作用有兩個(gè):
- 保證特定操作的執(zhí)行順序
- 保證某些變量的內(nèi)存可見性(
volatile
利用該特性實(shí)現(xiàn)可見性)
由于編譯器和處理器都能進(jìn)行執(zhí)行指令重排優(yōu)化媒殉,如果在指令間插入一條內(nèi)存屏障告訴編譯器和CPU担敌,不管什么指令都不能和這條內(nèi)存屏障指令重排序,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化廷蓉。內(nèi)存屏障另外一個(gè)作用就是強(qiáng)制刷出各種CPU的緩存數(shù)據(jù)全封,因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本
對(duì)volatile
變量進(jìn)行寫操作時(shí),會(huì)在寫操作后加一條store屏障指令桃犬,將工作內(nèi)存中的共享變量刷新回主內(nèi)存
對(duì)volatile
變量進(jìn)行讀操作時(shí)刹悴,會(huì)在讀操作前加一條load屏障指令,從主內(nèi)存中讀取共享變量
保證線程安全性
- 對(duì)于工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題攒暇,可以使用
synchronized
或volatile
關(guān)鍵字解決土匀,他們都可以是一個(gè)線程修改后的變量立即對(duì)其他線程可見 - 對(duì)于指令重排導(dǎo)致的可見性問題和有序性問題,可以使用
volatile
關(guān)鍵字解決扯饶,因?yàn)?code>volatile關(guān)鍵字可以禁止重排序優(yōu)化