??關(guān)于多線程的文章非常多,但是有一個特點,大多數(shù)文章都只講一個或幾個點,并沒有真正把知識串聯(lián)起來,沒有達(dá)到融匯貫通的效果。如何保證多線程操作的一致性問題,為何要用volatile,為何要用synchronized呢?我們要知其然,更要知其所以然。
??多線程操作,就是多核cpu一起操作某個共享變量,這個操作如果不進(jìn)行規(guī)范控制,那結(jié)果是不可預(yù)知的,也就是線程不安全,那么jvm是怎么來保證線程安全呢?下面用比較通俗的方式分為兩類,可能這樣分不太確切,但便于理解。
一、變量級別的一致性
volatile關(guān)鍵字
??cpu執(zhí)行時需要將數(shù)據(jù)從主內(nèi)存讀取到工作內(nèi)存(緩存)中執(zhí)行,而cpu的工作內(nèi)存是每個核都有對應(yīng)的獨立內(nèi)存空間,參與cpu運(yùn)算的數(shù)據(jù)都會從主內(nèi)存加載到工作內(nèi)存進(jìn)行操作。假如有兩個線程,一個線程在修改工作內(nèi)存的數(shù)據(jù),另一個線程同時在主存讀取數(shù)據(jù),如果沒有特殊規(guī)則,很可能會造成讀取數(shù)據(jù)時讀到了沒有更新的數(shù)據(jù),這就是不可見性。

??出現(xiàn)以上問題,就是因為i=6雖然在工作內(nèi)存已經(jīng)變了,但是不會立即刷新到主存中。為了解決緩存不一致性問題,通常硬件層面有以下兩種解決方法:
1.通過在總線加 LOCK# 鎖的方式
2.通過緩存一致性協(xié)議
第一種方式:通俗理解就是某一核cpu的獨占,可以比喻成排隊,當(dāng)某一核cpu在操作時會發(fā)出一個LOCK信號,在總線上鎖定,等數(shù)據(jù)刷到主存了再由另一核cpu操作該數(shù)據(jù)。這種方式效率是比較低的,現(xiàn)代的cpu一般不會采用。
第二種方式:就是規(guī)定如果操作的變量是多線程共享的,那么得遵循以下規(guī)則,
1.寫操作時必須立即把數(shù)據(jù)刷新到主存,并通過其它核的cpu把對應(yīng)工作內(nèi)存的數(shù)據(jù)變?yōu)槭А?br>
2.讀操作時直接從主存讀取。
Intel 的 MESI 協(xié)議就是解決這個問題的。
??但是java是號稱可以在所有環(huán)境中執(zhí)行的,那就必須爭對這個問題有一個好的解決方案,這個方案就是volatile關(guān)鍵字實現(xiàn)的內(nèi)存屏障。
內(nèi)存屏障分為兩種:
Load Barrier 讀屏障
Store Barrier 寫屏障
有兩個作用
1.寫的時候,強(qiáng)制把緩沖區(qū)/高速緩存中的數(shù)據(jù)寫回主內(nèi)存,并讓緩沖中的數(shù)據(jù)失效;讀的時候直接從主內(nèi)存中讀取。這個思路與Intel 的MESI很像,但這是程序級的實現(xiàn)。
2.阻止屏障兩側(cè)的指令重排序
指令重排又是怎么回事呢?指令重排是指在程序執(zhí)行過程中, 為了性能考慮, 編譯器和CPU可能會對指令重新排序。(這部分內(nèi)容后面描述)
??那么問題來了,使用了volatile關(guān)鍵字就可以解決線程安全問題了嗎?答案當(dāng)然是不行的,為何呢?因為變量共享操作的一致性是解決了,但程序級別的一致性沒解決。

??所以如果將變量i只用volatile關(guān)鍵字修飾,多線程編碼時是不能解決線程安全問題的。
CAS操作
??CAS操作是多線程經(jīng)常用到的操作,CAS:Compare and Swap,即比較再交換。我們熟悉的AtomicInteger、synchronized、Lock都是依賴CAS操作進(jìn)行的。CAS與volatile配合可以保證多線程的數(shù)據(jù)一致性,因為CAS需要兩個數(shù)據(jù),一個舊值,一個新值,如果現(xiàn)在主內(nèi)存中的值與舊值相等,那么就直接把舊值換成新值,否則不交換。概括一下,CAS其實就是樂觀鎖的思路,我要改一個值,前提是這個值必須等于我之前拿到的值,否則我不能改。
??CAS是原子操作,也就是說執(zhí)行比較交換這個指令是一條不可分割的指令,這是因為CPU硬件直接支持比較交換操作,所以這樣的操作是線程安全的。簡單說說AtomicInteger、synchronized、Lock是如何利用CAS操作的:
AtomicInteger:
??這是原子累計操作,不需要上鎖就可實現(xiàn)多線程的累加功能。其實現(xiàn)的本質(zhì)就是volatile+CAS,volatile保證了多線程之間的數(shù)據(jù)可見即可獲取最新的數(shù)據(jù),而CAS把獲取到最新的數(shù)據(jù)進(jìn)行+1。如果此時另一個線程先+1了,則這次的操作會失敗,會重新獲取到新的值繼續(xù)嘗試+1直到成功。
synchronized、Lock:
??原理是通過CAS操作去設(shè)置一個值,如果設(shè)置成功說明拿到了鎖,如果設(shè)置失敗那就會進(jìn)行短時間等待后再償試設(shè)置,如果還不成功則轉(zhuǎn)到等待隊列。鎖的操作離不開volatile+CAS,鎖的具體實現(xiàn)需要專門寫一篇文章講解,此處不再深入。
二、程序級別的一致性
先來看看下面這個經(jīng)典的雙重檢查鎖實現(xiàn)單例:
class Singleton{
private static volatile Singleton singleton;
private Singleton(){};
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
為何要寫一個單例要這么復(fù)雜呢?因為存在兩個問題:
1、多線程操作同一個變量
2、指令重排序
說說指令重排序:
問題出現(xiàn)在創(chuàng)建對象的語句singleton = new Singleton(); 上,在java中創(chuàng)建一個對象并非是一個原子操作,可以被分解成三行偽代碼:
//1:分配對象的內(nèi)存空間
memory = allocate();
//2:初始化對象
ctorInstance(memory);
//3:設(shè)置instance指向剛分配的內(nèi)存地址
instance = memory;
上面三行偽代碼中的2和3之間,可能會被重排序(在一些JIT編譯器中),即編譯器或處理器為提高性能改變代碼執(zhí)行順序,重排序之后的偽代碼是這樣的:
//1:分配對象的內(nèi)存空間
memory = allocate();
//3:設(shè)置instance指向剛分配的內(nèi)存地址
instance = memory;
//2:初始化對象
ctorInstance(memory);
??如果不進(jìn)行鎖定檢查,在多線程獲取單例時很可能獲取到一個指向了內(nèi)存地址但是沒有初始化的對象。singleton對象是用volatile修飾的,volatile可以保證不進(jìn)行指令重排序。另外在類對象上加了synchronized鎖,保證了同一個時間只能由一個線程執(zhí)行代碼塊的new操作,這樣就保證單例不會出空指針問題。當(dāng)然一般情況下建議直接通過餓漢模式直接new一個靜態(tài)類,也能保證單例,因為靜態(tài)類型是在類加載的時候已經(jīng)初始化好了。
以上就是對jvm保證多線程的數(shù)據(jù)一致性的一些理解。