介紹
線程安全的三大特性,原子性、可見性、有序性,這三大特性與我們之前整理的內(nèi)容息息相關(guān)。本篇重點介紹下volatile的底層原理,幫助我們更好的理解java并發(fā)包。
一、原子性
提供了互斥訪問,同一時刻只能有一個線程來對它進(jìn)行操作。
1. 原子性-synchronizes
2. 原子性-lock
- lock屬于jdk提供的代碼層面上的鎖,后面單獨總結(jié)。
3. 原子性-cas
4. 原子性-對比
- synchronized:不可中斷鎖(在作用范圍內(nèi)必須等待執(zhí)行完),適合競爭不激烈。
- Lock:可中斷鎖(unlock),競爭激烈時能保持性能常態(tài)。
- Atomic:競爭激烈時能保持性能常態(tài),比Lock性能好,只能同步一個值。
二、可見性
可見性指的是一個線程對主內(nèi)存的修改,可以被其他線程及時的觀察到。導(dǎo)致共享變量在線程間不可見的原因:
- 線程交叉執(zhí)行。
- 重排序結(jié)合線程交叉執(zhí)行。
- 共享變量更新后的值沒有在工作內(nèi)存與主內(nèi)存間及時更新。
1. 可見性-synchronizes
JMM中關(guān)于synchronized的內(nèi)存語意:
- 進(jìn)入synchronized塊的內(nèi)存語義是把synchronized塊內(nèi)使用到的變量從線程的工作內(nèi)存中清除,這樣synchronized塊內(nèi)使用到該變量就是直接從主內(nèi)存中獲取。
- 退出synchronized塊的內(nèi)存語義是把synchronized塊內(nèi)對共享變量的修改刷新到主內(nèi)存。
2. 可見性-volatile
通過加入內(nèi)存屏障和禁止重排序優(yōu)化來實現(xiàn)。
JMM中關(guān)于volatile的內(nèi)存語意:
- 當(dāng)線程寫入volatile變量值時就等價于線程退出synchronized同步塊(對volatile變量寫操作時,會在寫操作后加入一條store屏障指令,將本地內(nèi)存中的共享變量值刷新到主內(nèi)存)。
- 讀取volatile變量值時就相當(dāng)于進(jìn)入到同步塊(對volatile變量讀操作,會在讀操作前加入一條load屏障指令,從主內(nèi)存中讀取共享變量)。
當(dāng)一個變量被聲明為volatile時,線程在寫入變量時不會把值緩存在寄存器中,而是會把值刷新回主內(nèi)存。當(dāng)其他線程讀取該共享變量時,會從主內(nèi)存重新獲取最新值。
下面看一個volatile內(nèi)存可見性的例子:
public class VolatileCanSeeTest {
private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() - > {
log.info("init begin");
while(!initFlag) {
}
// if(!initFlag) {while(true){}} // JIT
log.info("===success===");
}).start();
Thread.sleep(1000);
new Thread(() - > doSomething()).start();
}
public static void doSomething() {
log.info("doSomething begin");
initFlag = true;
log.info("doSomething end");
}
}
查看對應(yīng)的匯編代碼,可以看到使用volatile匯編指令會加上lock前綴指令:
- 【rsp】是寄存器的意思,在java內(nèi)存模型一節(jié)中我們介紹了工作內(nèi)存就是寄存器以及cpu告訴緩存等的一個抽象概念。
- 這里值得一提的是,重排序只是編譯器優(yōu)化的一種表現(xiàn),上面這段代碼主要是編譯器優(yōu)化導(dǎo)致的。編譯器會認(rèn)為這段循環(huán)代碼在單線程運(yùn)行中,initFlag變量不會被改變,從而優(yōu)化為:
if(!initFlag) {
while(true){
}
}
這里結(jié)合java內(nèi)存模型對volatile底層原理進(jìn)行說明:
- 這里添加lock前綴指令的意思是當(dāng)cpu執(zhí)行引擎處理完共享變量的計算后,通過asign指令將共享變量回寫到工作內(nèi)存中后會立即將該共享變量通過store和write指令回寫到主內(nèi)存,并且給這兩個cpu指令加lock指令鎖。
- 同時,結(jié)合cpu緩存一致性協(xié)議,當(dāng)共享變量回寫主內(nèi)存時,經(jīng)過總線觸發(fā)MESI協(xié)議,另其他包含了該共享變量的緩存行置為無效狀態(tài),所以其他線程需要從主內(nèi)存中重新加載該共享變量到自己的工作內(nèi)存,從而保證了共享變量的內(nèi)存可見性。同時,結(jié)合[cpu緩存一致性協(xié)議,當(dāng)共享變量回寫主內(nèi)存時,經(jīng)過總線觸發(fā)MESI協(xié)議,另其他包含了該共享變量的緩存行置為無效狀態(tài),所以其他線程需要從主內(nèi)存中重新加載該共享變量到自己的工作內(nèi)存,從而保證了共享變量的內(nèi)存可見性。
3. 可見性-對比
- synchronized:保證可見性和原子性,但可能會導(dǎo)致線程上下文切換和增加重新調(diào)度的開銷。
- volatile:只能保證共享變量的可見性,不能解決讀-改-寫等的原子性問題。
- 關(guān)于共享內(nèi)存可見性以及JMM詳見:java內(nèi)存模型
三、有序性
一個線程觀察其他線程中的指令執(zhí)行順序,由于指令重排序的存在,該觀察結(jié)果一般雜亂無序。
1. 有序性-happens-before原則
java內(nèi)存模型中允許編譯器和處理器對指令進(jìn)行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。可以通過volatile關(guān)鍵字來保證一定的有序性,可以通過synchronized、lock保證同一時刻線程順序執(zhí)行來保證有序性。另外,java內(nèi)存模型具備先天的有序性,稱為**happens-before **原則:
-
程序次序規(guī)則(保證單線程的有序性,不保證多線程的有序性)
一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
-
鎖定規(guī)則
一個unlock操作先行發(fā)生于對后面同一個鎖的lock操作。
-
volatile變量規(guī)則
對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作。
-
傳遞規(guī)則
如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C。
-
線程啟動規(guī)則
Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
-
線程中斷規(guī)則
對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生。
-
線程終結(jié)規(guī)則
線程中所有的操作都先行發(fā)生于線程的終止檢測,可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行。
-
對象終結(jié)規(guī)則
一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始。
如果兩個操作的執(zhí)行順序無法從happens-before原則推導(dǎo)出來,那么就無法保證他們的有序性,虛擬機(jī)可以隨意的對他們重排序。
2. 有序性-synchronizes
首先,可以明確的一點是:synchronized是無法禁止指令重排和處理器優(yōu)化的。那么他是如何保證的有序性呢?
synchronized保證的有序性是多個線程之間的有序性,即被加鎖的內(nèi)容要按照順序被多個線程執(zhí)行。但是其內(nèi)部的同步代碼還是會發(fā)生重排序,只不過由于編譯器和處理器都遵循as-if-serial語義,所以我們可以認(rèn)為這些重排序在單線程內(nèi)部可忽略。
as-if-serial語義的意思指:不管怎么重排序,單線程程序的執(zhí)行結(jié)果都不能被改變。編譯器和處理器無論如何優(yōu)化,都必須遵守as-if-serial語義。簡單說就是,as-if-serial語義保證了單線程中,不管指令怎么重排,最終的執(zhí)行結(jié)果是不能被改變的。
3. 有序性-volatile
java內(nèi)存模型允許編譯器和處理器對指令重排序提高運(yùn)行性能,并且只會對不存在數(shù)據(jù)依賴性的指令重排序。例:
int a = 1;
int b = 2;
int c = a + b;
變量c的值依賴a和b的值,所以重排序后能保證c的操作在a,b之后,但是a,b誰先執(zhí)行就不一定,這在單線程下不存在問題。下面看一個多線程下指令重排序的例子:
public class VolatileSerialTest {
private static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException{
Set< String > resultSet = new HashSet< >();
Map< String, Integer > resultMap = new HashMap< >();
for (int i = 0; i < 1000000; i++) {
x = 0;
y = 0;
resultMap.clear();
Thread one = new Thread(() - > {
int a = y;
x = 1;
resultMap.put("a", a);
});
Thread two = new Thread(() - > {
int b = x;
y = 1;
resultMap.put("b", b);
});
one.start();
two.start();
one.join();
two.join();
resultSet.add("a=" + resultMap.get("a") + "," + "b=" + resultMap.get("b"));
log.info("ab結(jié)果:{}", resultSet);
}
}
}
由于指令重排序?qū)е驴赡艹霈F(xiàn)的結(jié)果有:
volatile禁止指令重排序原理:
volatile通過加入內(nèi)存屏障禁止指令重排序。 編譯器會根據(jù)volatile/synchronized/final等的語義,在特定的位置插入內(nèi)存屏障。 當(dāng)遇到特定的內(nèi)存屏障指令時,處理器將禁止其對應(yīng)的重排序,保證屏障前面的操作可以被后面的操作可見。
4. 有序性-對比
- synchronized是一種鎖機(jī)制,存在阻塞問題和性能問題,而volatile并不是鎖,所以不存在阻塞和性能問題。
- volatile借助了內(nèi)存屏障來幫助其解決可見性和有序性問題,而內(nèi)存屏障的使用還為其帶來了一個禁止指令重排的附件功能,所以在有些場景中是可以避免發(fā)生指令重排的問題的。
結(jié)語
本文總結(jié)了線程安全的三大特性,同時文中幾乎涉及到了所以之前總結(jié)過的知識,在閱讀過程中可以參考之前的文章進(jìn)行理解。至此,我們對并發(fā)包基礎(chǔ)應(yīng)該有了完整的認(rèn)識。
-
寄存器
+關(guān)注
關(guān)注
31文章
5343瀏覽量
120373 -
JAVA語言
+關(guān)注
關(guān)注
0文章
138瀏覽量
20095 -
volatile
+關(guān)注
關(guān)注
0文章
45瀏覽量
13031
發(fā)布評論請先 登錄
相關(guān)推薦
評論