線(xiàn)程池和ReentrantLock背后的最強(qiáng)支柱:volatile
一、前言
在前幾篇文章中,我們?cè)诜治鼍€(xiàn)程池和ReentrantLock的時(shí)候,其內(nèi)部實(shí)現(xiàn)大量用到了volatile關(guān)鍵字來(lái)修飾變量,前面我們也簡(jiǎn)單分析過(guò)使用volatile是為了用它的內(nèi)存可見(jiàn)性。除了內(nèi)存可見(jiàn)性,它還有哪些能力呢?這篇文章來(lái)詳細(xì)告訴你。
二、大象裝進(jìn)冰箱的case
給你一臺(tái)足夠大的冰箱,把大象塞進(jìn)去至少需要三步,第一步打開(kāi)冰箱門(mén),第二步將大象搬進(jìn)去,第三步將冰箱門(mén)關(guān)上。我們來(lái)假設(shè)一個(gè)場(chǎng)景:冰箱只有一臺(tái)且同一時(shí)刻只能放入一只大象,但在某一時(shí)刻有5只大象都要進(jìn)入冰箱降暑,那么在大象裝進(jìn)冰箱這件事情的整個(gè)過(guò)程中,中間任一步驟失敗就會(huì)直接導(dǎo)致整件事情的失敗。如果不想存在中間過(guò)程中出現(xiàn)失敗的可能,只有一個(gè)辦法這件事件的三個(gè)步驟合三為一,使其成為一個(gè)整體,從外部看就像只有一個(gè)“將大象塞進(jìn)冰箱”動(dòng)作。我們?cè)诙嗑€(xiàn)程環(huán)境下對(duì)一個(gè)變量進(jìn)行操作時(shí),會(huì)經(jīng)常遇到這種問(wèn)題,下面我們來(lái)看看如何完美解決。
二、Java內(nèi)存模型
想要完美解決多線(xiàn)程下對(duì)同一變量進(jìn)行安全操作,我們得先要了解清楚Java內(nèi)存模型,內(nèi)存模型如下圖所示
圖片
- Java內(nèi)存模型規(guī)定了所有的變量都必須存儲(chǔ)在主內(nèi)存中,而每條工作線(xiàn)程有自己的工作內(nèi)存,工作內(nèi)存中存儲(chǔ)的的是該線(xiàn)程執(zhí)行過(guò)程中臨時(shí)用到的變量信息,這些信息都是從主內(nèi)存中拷貝的副本,另外線(xiàn)程對(duì)變量的所有操作行為都必須在工作內(nèi)存完成,而不能直接操作主內(nèi)存中的變量信息。
- 不同線(xiàn)程之間也無(wú)法直接訪(fǎng)問(wèn)對(duì)方工作內(nèi)存中的變量,線(xiàn)程間變量的傳遞均需通過(guò)自己的工作內(nèi)存和主內(nèi)存之間進(jìn)行數(shù)據(jù)交互,然后再傳遞到別的線(xiàn)程工作內(nèi)存中完成信息的交互。
小結(jié):JMM(Java Memory Model)是一種規(guī)范,目的是解決由于多線(xiàn)程通過(guò)共享內(nèi)存進(jìn)行通信時(shí),存儲(chǔ)在工作內(nèi)存的數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來(lái)的問(wèn)題。
三、volatile三大屬性
2.1 原子性
2.1.1 volatile為什么不能保證原子性
/**
* @author 程序反思錄 <程序反思錄@xxx.com>
* Created on 2024-09-29
*/
public class MultiThreadCount {
private volatile int salesCount = 0;
public void addSalesCount() {
salesCount++;
}
public static void main(String[] args) {
MultiThreadCount multiThreadCount = new MultiThreadCount();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
multiThreadCount.addSalesCount();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
multiThreadCount.addSalesCount();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
multiThreadCount.addSalesCount();
}
}).start();
System.out.println(multiThreadCount.salesCount);
}
}
運(yùn)行上面這段代碼,在不同的機(jī)器上得到的結(jié)果大概率都不一樣且結(jié)果值都不是3000。
現(xiàn)在我們?cè)倩剡^(guò)頭來(lái)分析上面的那段示例代碼,剛開(kāi)始3個(gè)線(xiàn)程分別從主內(nèi)存copy salesCount=0到各自的工作內(nèi)存中去,然后分別執(zhí)行自增操作,完后后將各自的值刷回到主內(nèi)存,一次salesCount自增操作會(huì)涉及三步操作(就像將大象放入冰箱的case一樣),多個(gè)線(xiàn)程同時(shí)多次執(zhí)行這三步操作勢(shì)必會(huì)造成主內(nèi)存中值被覆蓋情況,這也就解釋了volatile沒(méi)能保證原子性的原因。
2.1.2 如何實(shí)現(xiàn)原子性
解決上面的問(wèn)題很容易,只需要將salesCount的修飾由volatile改成就可以了,代碼如下
private AtomicInteger salesCount = new AtomicInteger(0);
public void addSalesCount() {
salesCount.incrementAndGet();
}
有同學(xué)就會(huì)好奇了,為什么AtomicInteger就可以解決數(shù)據(jù)被刷回到主內(nèi)存后數(shù)據(jù)被覆蓋的問(wèn)題呢?點(diǎn)開(kāi)AtomicInteger的源碼會(huì)有有兩個(gè)關(guān)鍵的動(dòng)作:
- AtomicInteger內(nèi)部維護(hù)的value屬性是用volatile修飾的,利用其內(nèi)存可見(jiàn)性的特性使得值被修改后,別的線(xiàn)程能夠及時(shí)感知到(后面分析內(nèi)存可見(jiàn)性的時(shí)候再展開(kāi))
- 使用了CAS特性加死循環(huán)來(lái)保證值不會(huì)被覆蓋,并將當(dāng)前最新值累加上去刷回到主內(nèi)存,我們稍微展開(kāi)分析一下具體實(shí)現(xiàn)
// 調(diào)用該方法對(duì)計(jì)數(shù)器進(jìn)行+1操作
public final int incrementAndGet() {
// 通過(guò)unsafe類(lèi)實(shí)現(xiàn)原子加+1操作
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 1. 首先通過(guò)CAS嘗試將+1后的數(shù)據(jù)寫(xiě)入到工作線(xiàn)程,
// 然后回寫(xiě)到主內(nèi)存(這里會(huì)通過(guò)lock指令強(qiáng)制將修改后的值回寫(xiě)到主內(nèi)存,
// 下面分析可見(jiàn)性的時(shí)候在展開(kāi))。
// 2. 如果CAS操作失敗了,通過(guò)while死循環(huán)不斷自旋,直到最新值被成功回寫(xiě)到主內(nèi)存,
// 說(shuō)點(diǎn)題外話(huà),相信看過(guò)線(xiàn)程池和ReentrantLock文章的同學(xué)會(huì)有感覺(jué),
// 一般CAS出現(xiàn)的地方,會(huì)伴隨著死循環(huán)的身影出現(xiàn)。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
2.2 內(nèi)存可見(jiàn)性
2.2.1 什么是內(nèi)存可見(jiàn)性
內(nèi)存可見(jiàn)性(Memory Visibility)是指在一個(gè)線(xiàn)程中修改了某個(gè)變量的值之后,這些修改能夠被其他線(xiàn)程立即看到。在多線(xiàn)程環(huán)境中,由于每個(gè)線(xiàn)程可能有自己的工作內(nèi)存(緩存),而不是直接操作主內(nèi)存,因此會(huì)出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題。
2.2.2 volatile是如何解決內(nèi)存可見(jiàn)性的問(wèn)題
當(dāng)對(duì)volatile修飾的變量進(jìn)行修改時(shí),JVM會(huì)向處理器發(fā)送一條lock前綴的指令,將當(dāng)前處理器中緩存的最新值強(qiáng)制寫(xiě)回到主存中,所有處理器都需要遵守緩存一致性協(xié)議,當(dāng)其他處理器發(fā)現(xiàn)自己緩存的數(shù)據(jù)已經(jīng)被修改,則會(huì)從主存中拉取最新的值緩存到自己的緩存內(nèi),從而實(shí)現(xiàn)了可見(jiàn)性的特性。 緩存一致性協(xié)議:每個(gè)處理器通過(guò)嗅探在總線(xiàn)上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是否已過(guò)期,當(dāng)處理器發(fā)現(xiàn)自己緩存行的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存設(shè)置成無(wú)效狀態(tài),當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作時(shí),會(huì)強(qiáng)制從主存讀取最新數(shù)據(jù)寫(xiě)入到處理器緩存中。
2.2.3 解決內(nèi)存可見(jiàn)性問(wèn)題的替代方案
i) 通過(guò)鎖來(lái)解決同一時(shí)刻只有一個(gè)線(xiàn)程可以修改值
- 使用synchroized關(guān)鍵字,保證多個(gè)線(xiàn)程操作時(shí),只有搶到鎖的線(xiàn)程才可以執(zhí)行修改操作
- 使用Atomic類(lèi),通過(guò)CAS+死循環(huán)的方式
ii) 使用final關(guān)鍵字修飾,使得變量不能被修改,從而避開(kāi)了內(nèi)存可見(jiàn)性問(wèn)題的發(fā)生
2.3 指令重排
2.3.1 什么是指令重排
指令重排是指編譯器、運(yùn)行時(shí)系統(tǒng)或處理器為了優(yōu)化性能,對(duì)程序中的指令順序進(jìn)行調(diào)整的過(guò)程。
2.3.2 指令重排有什么好處
i) 編譯器優(yōu)化:編譯器可能會(huì)對(duì)代碼進(jìn)行重排序,以減少寄存器的使用、提高指令流水線(xiàn)的效率等。
ii) 運(yùn)行時(shí)系統(tǒng)優(yōu)化:運(yùn)行時(shí)系統(tǒng)可能會(huì)對(duì)字節(jié)碼進(jìn)行優(yōu)化,以提高執(zhí)行效率。
iii) 處理器優(yōu)化:現(xiàn)代處理器具有復(fù)雜的流水線(xiàn)和多級(jí)緩存,可能會(huì)對(duì)指令進(jìn)行重排序以提高性能。
2.3.3 為什么volatile禁止指令重排
大多數(shù)情況下指令重排這種優(yōu)化操作是透明的,但在多線(xiàn)程環(huán)境中,指令重排可能會(huì)導(dǎo)致一些問(wèn)題 i) 內(nèi)存可見(jiàn)性問(wèn)題:由于指令執(zhí)行順序被重排,使得修改操作被延遲觸發(fā),最終導(dǎo)致一個(gè)線(xiàn)程對(duì)變量的修改可能不會(huì)理解對(duì)其他線(xiàn)程可見(jiàn)。ii) 競(jìng)態(tài)條件:指令重排可能導(dǎo)致兩個(gè)線(xiàn)程之間的操作順序不符合預(yù)期,從而引發(fā)競(jìng)態(tài)條件。
2.3.4 禁止指令重排是如何實(shí)現(xiàn)的
禁止指令重排序是通過(guò)內(nèi)存屏障來(lái)實(shí)現(xiàn)的。內(nèi)存屏障是一種特殊的指令,它可以確保某些操作在屏障前后按照特定的順序執(zhí)行,從而防止編譯器、運(yùn)行時(shí)系統(tǒng)和處理器對(duì)這些操作進(jìn)行重排序。內(nèi)存屏障分為兩種:i) 寫(xiě)屏障:在寫(xiě)操作之后插入一個(gè)寫(xiě)屏障,確保所有之前的寫(xiě)操作都已完成并回寫(xiě)到主內(nèi)存中。ii) 讀屏障:在讀操作之前插入一個(gè)讀屏障,確保所有后續(xù)的讀操作都從主內(nèi)存中讀取最新的值。
四、后續(xù)
本篇文章從volatile的特性展開(kāi),介紹到了Java的JMM(Java內(nèi)存模型)模型,有些同學(xué)這個(gè)時(shí)候心里就要開(kāi)始迷糊了,我聽(tīng)過(guò)Java對(duì)象模型、JVM內(nèi)存模型,那它們又是干什么用的呢?我知道你很急,但是你不用急,下篇文章接著解答的疑惑。