一北秽、死磕Java——volatile的理解
1.1.JMM內(nèi)存模型
理解volatile
的相關(guān)知識(shí)前,先簡(jiǎn)單的認(rèn)識(shí)一下JMM(Java Memory Model
),JMM是jdk5
引入的一種jvm
的一種規(guī)范勘高,本身是一種抽象的概念,并不真實(shí)存在绰精,它屏蔽了各種硬件和操作系統(tǒng)的訪問(wèn)差異扛点,它的目的是為了解決由于多線程通過(guò)共享數(shù)據(jù)進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致续镇、編譯器會(huì)對(duì)代碼進(jìn)行指令重排等問(wèn)題美澳。
JMM有關(guān)同步的規(guī)定:
- 線程解鎖前,必須把共享變量的值刷新回主內(nèi)存摸航;
- 線程加鎖前制跟,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存中;
- 加鎖和解鎖使用的是同一把鎖忙厌;
關(guān)于上述規(guī)定如下圖解:
說(shuō)明:當(dāng)我們?cè)诔绦蛑?code>new一個(gè)user
對(duì)象的時(shí)候凫岖,這個(gè)對(duì)象就存在我們的主內(nèi)存中,當(dāng)多個(gè)線程操作主內(nèi)存的name
變量的時(shí)候逢净,會(huì)先將user
對(duì)象中的name
屬性進(jìn)行拷貝一份到自己線程的工作內(nèi)存中,自己修改自己工作內(nèi)存中的屬性后歼指,再將修改后的屬性值刷新回主內(nèi)存爹土,這就會(huì)存在一些問(wèn)題,例如踩身,一個(gè)線程寫完胀茵,還沒(méi)有寫回到主內(nèi)存,另一個(gè)線程先修改后寫入到主內(nèi)存挟阻,就會(huì)存在數(shù)據(jù)的丟失或者臟數(shù)據(jù)琼娘。所以峭弟,JMM就存在如下規(guī)定:
- 可見性
- 原子性
- 有序性
1.2.Volatile關(guān)鍵字
volatile
是java
虛擬機(jī)提供的一種輕量級(jí)的同步機(jī)制,比較與synchronized
脱拼。我們知道的事volatile
的三大特性:
- 可見性
- 不保證原子性
- 禁止指令重排
1.2.1.Volatile如何保證可見性
可見性就是當(dāng)多個(gè)線程操作主內(nèi)存的共享數(shù)據(jù)的時(shí)候瞒瘸,當(dāng)其中一個(gè)線程修改了數(shù)據(jù)寫回主內(nèi)存的時(shí)候,回立刻通知其他線程熄浓,這就是線程的可見性情臭。先看一個(gè)簡(jiǎn)單的例子:
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é)束");
}
}
這是一個(gè)簡(jiǎn)單的示例程序,存在一個(gè)兩個(gè)線程赌蔑,一個(gè)子線程修改主內(nèi)存的共享數(shù)據(jù)num
的值俯在,main
線程使用while
時(shí)時(shí)檢測(cè)自己是否是道主內(nèi)存的num
的值是否被改變,運(yùn)行程序程序執(zhí)行結(jié)束并不會(huì)被打印娃惯,同時(shí)跷乐,程序也不會(huì)停止。這就是線程之間的不可見問(wèn)題趾浅,解決方法就是可以添加volatile關(guān)鍵字
劈猿,修改如下:
volatile int num = 0;
1.2.2.Volatile保證可見性的原理
將Java
程序生成匯編代碼的時(shí)候,我們可以看見潮孽,當(dāng)我們對(duì)添加了volatile
關(guān)鍵字修飾的變量時(shí)候揪荣,會(huì)多出一條Lock
前綴的的指令。我們知道的是cpu
不直接與主內(nèi)存進(jìn)行數(shù)據(jù)交換往史,中間存在一個(gè)高速緩存區(qū)域仗颈,通常是一級(jí)緩存、二級(jí)緩存和三級(jí)緩存椎例,而添加了volatile
關(guān)鍵字進(jìn)行操作時(shí)候挨决,生成的Lock
前綴的匯編指令主要有以下兩個(gè)作用:
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存;
- 這個(gè)寫回內(nèi)存的操作會(huì)使得其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效订歪;
Idea查看程序的匯編指令在VM啟動(dòng)參數(shù)配上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly即可脖祈;
參考:https://wiki.openjdk.java.net/display/HotSpot/PrintAssembly
在多處理器下,為了保證各個(gè)處理器的緩存是一致的刷晋,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議盖高,每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改眼虱,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài)喻奥,當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里捏悬。
總結(jié):Volatile通過(guò)緩存一致性保證可見性撞蚕。
1.2.3.Volatile不保證原子性
原子性:也可以說(shuō)是保持?jǐn)?shù)據(jù)的完整一致性,也就是說(shuō)當(dāng)某一個(gè)線程操作每一個(gè)業(yè)務(wù)的時(shí)候过牙,不能被其他線程打斷甥厦,不可以被分割操作纺铭,即整體一致性,要么同時(shí)成功刀疙,要么同時(shí)失敗舶赔。
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)時(shí)庙洼,打印的最終結(jié)果幾乎很難為20000顿痪,這就很充分的說(shuō)明了volatile
并不能保證數(shù)據(jù)的原子性,這里的num++
操作油够,雖然只有一行代碼蚁袭,但是實(shí)際是三步操作,這也是為什么i++
在多線程下是非線程安全的石咬。
1.2.4.為什么Volatile不保證原子性
可以參考JMM模型的那一張圖揩悄,就是主內(nèi)存中存在一個(gè)num = 0
,當(dāng)其中一個(gè)線程將其修改為1鬼悠,然后將其寫回主內(nèi)存的時(shí)候删性,就被掛起了,另外一個(gè)線程也將主內(nèi)存的num = 0
修改為1焕窝,然后寫入后蹬挺,之前的線程被喚醒,快速的寫入主內(nèi)存它掂,覆蓋了已經(jīng)寫入的1巴帮,造成了數(shù)據(jù)丟失操作,兩次操作最終結(jié)果應(yīng)該為2虐秋,但是為1榕茧,這就是為什么會(huì)造成數(shù)據(jù)丟失。再來(lái)看i++
對(duì)應(yīng)的字節(jié)碼
簡(jiǎn)單翻譯一下字節(jié)碼的操作:
- aload_0:從局部變量表的相應(yīng)位置裝載一個(gè)對(duì)象引用到操作數(shù)棧的棧頂客给;
- dup:復(fù)制棧頂元素用押;
- getfield:先獲得原始值;
- iadd:進(jìn)行+1操作靶剑;
- putfield:再把累加后的值寫回主內(nèi)存操作蜻拨;
1.2.5.解決Volatile不保證原子性的問(wèn)題
使用AtomicInteger
來(lái)保證原子性,有關(guān)AtomicInteger
的詳細(xì)知識(shí)抬虽,后面在死磕官觅,官方文檔截圖如下:
修改之前的不保證原子性的代碼如下:
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的禁止指令重排序
首先,假如寫了如下代碼
在程序中阐污,我們覺(jué)得是會(huì)依次順序執(zhí)行,但是在計(jì)算機(jī)在執(zhí)行程序的時(shí)候咱圆,為了提高性能笛辟,編譯器和和處理器通常會(huì)對(duì)指令進(jìn)行指令重排序功氨,可能執(zhí)行順序?yàn)椋?—1—3—4,也可能是:1—3—2—4手幢,一般分為下面三種:
雖然處理器會(huì)對(duì)指令進(jìn)行重排捷凄,但是同時(shí)也會(huì)遵守一些規(guī)則,例如上述代碼不可能重排后將第四句代碼第一個(gè)執(zhí)行围来,所以跺涤,單線程下確保程序的最終執(zhí)行結(jié)果和順序執(zhí)行結(jié)一致,這就是處理器在進(jìn)行指令重排序時(shí)候必須考慮的就是指令之間的數(shù)據(jù)依賴性监透。
但是桶错,在多線程環(huán)境下,由于編譯器重排的存在胀蛮,兩個(gè)線程使用的變量能否保證一致性無(wú)法確定院刁,所以結(jié)果就無(wú)法一致。在看一個(gè)示例:
在多線程環(huán)境下粪狼,第一種就是順序執(zhí)行init方法退腥,先將num
進(jìn)行賦值操作,在執(zhí)行update
方法再榄,結(jié)果:num
為6狡刘,但是存在編譯器重排,那么可能先執(zhí)行falg = true;
再執(zhí)行num = 1;
困鸥,最終num
為5嗅蔬;
1.2.7.Volatile禁止指令重排序的原理
前面說(shuō)到了volatile
禁止指令重排優(yōu)化,從而避免在多線程環(huán)境下出現(xiàn)結(jié)果錯(cuò)亂的現(xiàn)象窝革。這是因?yàn)樵?code>volatile會(huì)在指令之間插入一條內(nèi)存屏障指令购城,通過(guò)內(nèi)存屏障指令告訴CPU
和編譯器不管什么指令,都不進(jìn)行指令重新排序虐译。也就說(shuō)說(shuō)通過(guò)插入的內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行指令重新排序優(yōu)化瘪板。
什么是內(nèi)存屏障
內(nèi)存屏障是一個(gè)CPU
指令,他的作用有兩個(gè):
- 保證特定操作的執(zhí)行順序漆诽;
- 保證某些變量的內(nèi)存可見性侮攀;
將上述代碼修改為:
volatile int num = 0;
volatile boolean falg = false;
這樣就保證執(zhí)行init
方法的時(shí)候一定是先執(zhí)行num = 1;
再執(zhí)行falg = true;
,就避免的了結(jié)果出錯(cuò)的現(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;
}
}