一、死磕Java——volatile的理解
1.1.JMM內(nèi)存模型
理解volatile的相關(guān)知識前改化,先簡單的認(rèn)識一下JMM(Java Memory Model),JMM是jdk5引入的一種jvm的一種規(guī)范佩憾,本身是一種抽象的概念哮伟,并不真實(shí)存在干花,它屏蔽了各種硬件和操作系統(tǒng)的訪問差異,它的目的是為了解決由于多線程通過共享數(shù)據(jù)進(jìn)行通信時楞黄,存在的本地內(nèi)存數(shù)據(jù)不一致池凄、編譯器會對代碼進(jìn)行指令重排等問題。
JMM有關(guān)同步的規(guī)定:
- 線程解鎖前鬼廓,必須把共享變量的值刷新回主內(nèi)存肿仑;
- 線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存中碎税;
- 加鎖和解鎖使用的是同一把鎖尤慰;
關(guān)于上述規(guī)定如下圖解:
說明:當(dāng)我們在程序中new一個user對象的時候,這個對象就存在我們的主內(nèi)存中雷蹂,當(dāng)多個線程操作主內(nèi)存的name變量的時候伟端,會先將user對象中的name屬性進(jìn)行拷貝一份到自己線程的工作內(nèi)存中,自己修改自己工作內(nèi)存中的屬性后匪煌,再將修改后的屬性值刷新回主內(nèi)存责蝠,這就會存在一些問題,例如萎庭,一個線程寫完霜医,還沒有寫回到主內(nèi)存,另一個線程先修改后寫入到主內(nèi)存驳规,就會存在數(shù)據(jù)的丟失或者臟數(shù)據(jù)支子。所以,JMM就存在如下規(guī)定:
- 可見性
- 原子性
- 有序性
1.2.Volatile關(guān)鍵字
volatile是java虛擬機(jī)提供的一種輕量級的同步機(jī)制达舒,比較與synchronized值朋。我們知道的事volatile的三大特性:
- 可見性
- 不保證原子性
- 禁止指令重排
1.2.1.Volatile如何保證可見性
可見性就是當(dāng)多個線程操作主內(nèi)存的共享數(shù)據(jù)的時候,當(dāng)其中一個線程修改了數(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的值是否被改變禽捆,運(yùn)行程序程序執(zhí)行結(jié)束并不會被打印笙什,同時,程序也不會停止胚想。這就是線程之間的不可見問題琐凭,解決方法就是可以添加volatile關(guān)鍵字,修改如下:
volatile int num = 0;
1.2.2.Volatile保證可見性的原理
將Java程序生成匯編代碼的時候浊服,我們可以看見统屈,當(dāng)我們對添加了volatile關(guān)鍵字修飾的變量時候胚吁,會多出一條Lock前綴的的指令。我們知道的是cpu不直接與主內(nèi)存進(jìn)行數(shù)據(jù)交換愁憔,中間存在一個高速緩存區(qū)域腕扶,通常是一級緩存、二級緩存和三級緩存吨掌,而添加了volatile關(guān)鍵字進(jìn)行操作時候半抱,生成的Lock前綴的匯編指令主要有以下兩個作用:
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存;
- 這個寫回內(nèi)存的操作會使得其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效膜宋;
Idea查看程序的匯編指令在VM啟動參數(shù)配上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly即可代虾;
在多處理器下,為了保證各個處理器的緩存是一致的激蹲,就會實(shí)現(xiàn)緩存一致性協(xié)議棉磨,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改学辱,就會將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài)乘瓤,當(dāng)處理器對這個數(shù)據(jù)進(jìn)行修改操作的時候,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里策泣。
總結(jié):Volatile通過緩存一致性保證可見性衙傀。
1.2.3.Volatile不保證原子性
原子性:也可以說是保持?jǐn)?shù)據(jù)的完整一致性,也就是說當(dāng)某一個線程操作每一個業(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();
}
}, "當(dāng)前子線程為線程" + String.valueOf(i)).start();
}
// 等待所有線程執(zhí)行結(jié)束
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("最終結(jié)果:" + data.num);
}
}
上述代碼就是在共享數(shù)據(jù)前添加了volatile關(guān)鍵字茫陆,當(dāng)時金麸,打印的最終結(jié)果幾乎很難為20000,這就很充分的說明了volatile并不能保證數(shù)據(jù)的原子性簿盅,這里的num++操作挥下,雖然只有一行代碼,但是實(shí)際是三步操作桨醋,這也是為什么i++在多線程下是非線程安全的棚瘟。
1.2.4.為什么Volatile不保證原子性
可以參考JMM模型的那一張圖,就是主內(nèi)存中存在一個num = 0喜最,當(dāng)其中一個線程將其修改為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:進(jìn)行+1操作忆谓;
- putfield:再把累加后的值寫回主內(nèi)存操作;
1.2.5.解決Volatile不保證原子性的問題
使用AtomicInteger來保證原子性踱承,有關(guān)AtomicInteger的詳細(xì)知識倡缠,后面在死磕,官方文檔截圖如下:
修改之前的不保證原子性的代碼如下:
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();
}
}, "當(dāng)前子線程為線程" + String.valueOf(i)).start();
}
// 等待所有線程執(zhí)行結(jié)束
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("最終結(jié)果:" + data.atomicInteger);
}
}
1.2.6.Volatile的禁止指令重排序
在程序中茎活,我們覺得是會依次順序執(zhí)行昙沦,但是在計算機(jī)在執(zhí)行程序的時候,為了提高性能载荔,編譯器和和處理器通常會對指令進(jìn)行指令重排序盾饮,可能執(zhí)行順序?yàn)椋?—1—3—4,也可能是:1—3—2—4懒熙,一般分為下面三種:
雖然處理器會對指令進(jìn)行重排丘损,但是同時也會遵守一些規(guī)則,例如上述代碼不可能重排后將第四句代碼第一個執(zhí)行工扎,所以号俐,單線程下確保程序的最終執(zhí)行結(jié)果和順序執(zhí)行結(jié)一致,這就是處理器在進(jìn)行指令重排序時候必須考慮的就是指令之間的數(shù)據(jù)依賴性定庵。
但是吏饿,在多線程環(huán)境下,由于編譯器重排的存在蔬浙,兩個線程使用的變量能否保證一致性無法確定猪落,所以結(jié)果就無法一致。在看一個示例:
在多線程環(huán)境下畴博,第一種就是順序執(zhí)行init方法笨忌,先將num進(jìn)行賦值操作,在執(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)象。這是因?yàn)樵趘olatile會在指令之間插入一條內(nèi)存屏障指令维费,通過內(nèi)存屏障指令告訴CPU和編譯器不管什么指令果元,都不進(jìn)行指令重新排序。也就說說通過插入的內(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;
}
}