一、死磕Java——volatile的理解
1.1.JMM內(nèi)存模型
理解volatile的相關(guān)知識前,先簡單的認識一下JMM(Java Memory Model),JMM是jdk5引入的一種jvm的一種規(guī)范,本身是一種抽象的概念,并不真實存在,它屏蔽了各種硬件和操作系統(tǒng)的訪問差異,它的目的是為了解決由于多線程通過共享數(shù)據(jù)進行通信時,存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會對代碼進行指令重排等問題。
JMM有關(guān)同步的規(guī)定:
- 線程解鎖前,必須把共享變量的值刷新回主內(nèi)存;
- 線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存中;
- 加鎖和解鎖使用的是同一把鎖;
關(guān)于上述規(guī)定如下圖解:

說明:當我們在程序中new一個user對象的時候,這個對象就存在我們的主內(nèi)存中,當多個線程操作主內(nèi)存的name變量的時候,會先將user對象中的name屬性進行拷貝一份到自己線程的工作內(nèi)存中,自己修改自己工作內(nèi)存中的屬性后,再將修改后的屬性值刷新回主內(nèi)存,這就會存在一些問題,例如,一個線程寫完,還沒有寫回到主內(nèi)存,另一個線程先修改后寫入到主內(nèi)存,就會存在數(shù)據(jù)的丟失或者臟數(shù)據(jù)。所以,JMM就存在如下規(guī)定:
- 可見性
- 原子性
- 有序性
1.2.Volatile關(guān)鍵字
volatile是java虛擬機提供的一種輕量級的同步機制,比較與synchronized。我們知道的事volatile的三大特性:
- 可見性
- 不保證原子性
- 禁止指令重排
1.2.1.Volatile如何保證可見性
可見性就是當多個線程操作主內(nèi)存的共享數(shù)據(jù)的時候,當其中一個線程修改了數(shù)據(jù)寫回主內(nèi)存的時候,回立刻通知其他線程,這就是線程的可見性。先看一個簡單的例子:
class MyDataDemo {
int num = 0;
public void updateNum() {
this.num = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyDataDemo myData = new MyDataDemo();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.updateNum();
System.out.println("num的值:" + myData.num);
}, "子線程").start();
while (myData.num == 0) {}
System.out.println("程序執(zhí)行結(jié)束");
}
}
這是一個簡單的示例程序,存在一個兩個線程,一個子線程修改主內(nèi)存的共享數(shù)據(jù)num的值,main線程使用while時時檢測自己是否是道主內(nèi)存的num的值是否被改變,運行程序程序執(zhí)行結(jié)束并不會被打印,同時,程序也不會停止。這就是線程之間的不可見問題,解決方法就是可以添加volatile關(guān)鍵字,修改如下:
volatile int num = 0;
1.2.2.Volatile保證可見性的原理
將Java程序生成匯編代碼的時候,我們可以看見,當我們對添加了volatile關(guān)鍵字修飾的變量時候,會多出一條Lock前綴的的指令。我們知道的是cpu不直接與主內(nèi)存進行數(shù)據(jù)交換,中間存在一個高速緩存區(qū)域,通常是一級緩存、二級緩存和三級緩存,而添加了volatile關(guān)鍵字進行操作時候,生成的Lock前綴的匯編指令主要有以下兩個作用:
- 將當前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存;
- 這個寫回內(nèi)存的操作會使得其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效;
Idea查看程序的匯編指令在VM啟動參數(shù)配上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly即可;
在多處理器下,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當前處理器的緩存行設(shè)置成無效狀態(tài),當處理器對這個數(shù)據(jù)進行修改操作的時候,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。
總結(jié):Volatile通過緩存一致性保證可見性。
1.2.3.Volatile不保證原子性
原子性:也可以說是保持數(shù)據(jù)的完整一致性,也就是說當某一個線程操作每一個業(yè)務(wù)的時候,不能被其他線程打斷,不可以被分割操作,即整體一致性,要么同時成功,要么同時失敗。
class MyDataDemo {
volatile int num = 0;
public void addNum() {
num++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyDataDemo data = new MyDataDemo();
for(int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j < 1000; j++) {
data.addNum();
}
}, "當前子線程為線程" + String.valueOf(i)).start();
}
// 等待所有線程執(zhí)行結(jié)束
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("最終結(jié)果:" + data.num);
}
}
上述代碼就是在共享數(shù)據(jù)前添加了volatile關(guān)鍵字,當時,打印的最終結(jié)果幾乎很難為20000,這就很充分的說明了volatile并不能保證數(shù)據(jù)的原子性,這里的num++操作,雖然只有一行代碼,但是實際是三步操作,這也是為什么i++在多線程下是非線程安全的。
1.2.4.為什么Volatile不保證原子性
可以參考JMM模型的那一張圖,就是主內(nèi)存中存在一個num = 0,當其中一個線程將其修改為1,然后將其寫回主內(nèi)存的時候,就被掛起了,另外一個線程也將主內(nèi)存的num = 0修改為1,然后寫入后,之前的線程被喚醒,快速的寫入主內(nèi)存,覆蓋了已經(jīng)寫入的1,造成了數(shù)據(jù)丟失操作,兩次操作最終結(jié)果應(yīng)該為2,但是為1,這就是為什么會造成數(shù)據(jù)丟失。再來看i++對應(yīng)的字節(jié)碼

簡單翻譯一下字節(jié)碼的操作:
- aload_0:從局部變量表的相應(yīng)位置裝載一個對象引用到操作數(shù)棧的棧頂;
- dup:復(fù)制棧頂元素;
- getfield:先獲得原始值;
- iadd:進行+1操作;
- putfield:再把累加后的值寫回主內(nèi)存操作;
1.2.5.解決Volatile不保證原子性的問題
使用AtomicInteger來保證原子性,有關(guān)AtomicInteger的詳細知識,后面在死磕,官方文檔截圖如下:

修改之前的不保證原子性的代碼如下:
class MyDataDemo {
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomicInteger() {
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyDataDemo data = new MyDataDemo();
for(int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
data.addAtomicInteger();
}
}, "當前子線程為線程" + String.valueOf(i)).start();
}
// 等待所有線程執(zhí)行結(jié)束
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("最終結(jié)果:" + data.atomicInteger);
}
}
1.2.6.Volatile的禁止指令重排序
在程序中,我們覺得是會依次順序執(zhí)行,但是在計算機在執(zhí)行程序的時候,為了提高性能,編譯器和和處理器通常會對指令進行指令重排序,可能執(zhí)行順序為:2—1—3—4,也可能是:1—3—2—4,一般分為下面三種:

雖然處理器會對指令進行重排,但是同時也會遵守一些規(guī)則,例如上述代碼不可能重排后將第四句代碼第一個執(zhí)行,所以,單線程下確保程序的最終執(zhí)行結(jié)果和順序執(zhí)行結(jié)一致,這就是處理器在進行指令重排序時候必須考慮的就是指令之間的數(shù)據(jù)依賴性。
但是,在多線程環(huán)境下,由于編譯器重排的存在,兩個線程使用的變量能否保證一致性無法確定,所以結(jié)果就無法一致。在看一個示例:

在多線程環(huán)境下,第一種就是順序執(zhí)行init方法,先將num進行賦值操作,在執(zhí)行update方法,結(jié)果:num為6,但是存在編譯器重排,那么可能先執(zhí)行falg = true;再執(zhí)行num = 1;,最終num為5;
1.2.7.Volatile禁止指令重排序的原理
前面說到了volatile禁止指令重排優(yōu)化,從而避免在多線程環(huán)境下出現(xiàn)結(jié)果錯亂的現(xiàn)象。這是因為在volatile會在指令之間插入一條內(nèi)存屏障指令,通過內(nèi)存屏障指令告訴CPU和編譯器不管什么指令,都不進行指令重新排序。也就說說通過插入的內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行指令重新排序優(yōu)化。
什么是內(nèi)存屏障
內(nèi)存屏障是一個CPU指令,他的作用有兩個:
- 保證特定操作的執(zhí)行順序;
- 保證某些變量的內(nèi)存可見性;
將上述代碼修改為:
volatile int num = 0;
volatile boolean falg = false;
這樣就保證執(zhí)行init方法的時候一定是先執(zhí)行num = 1;再執(zhí)行falg = true;,就避免的了結(jié)果出錯的現(xiàn)象。
1.3.Volatile的單例模式
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo(){};
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
}