前言
volatile關鍵字能夠保證可見性和有序性,但是volatile為什么能夠保證可見性和有序性?為什么volatile又不能保證原子性?
今天,我們從CPU多核緩存架構出發,結合MESI緩存一致性協議來深入剖析一下,volatile的原理。
問題的出現
我們先通過一個例子來看看,可見性導致的線程安全問題:
java callable接口和runnable?public class Main {
static int a = 0;
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
java線程等待?while (a == 0) {
}
System.out.println("T1得知a = 1");
}
});
Thread t2 = new Thread(new Runnable() {
Java 線程,@Override
public void run() {
try {
Thread.sleep(1000);
a = 1;
System.out.println("T2修改a = 1");
java instanceof,} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
java線程通信?t2.start();
}
}
線程T2再休眠1秒之后,修改了a的值為1,此時T1應該退出while循環并打印,但是結果并非如此:
T1沒有退出循環,程序也就不會結束。但是如果對a使用volatile關鍵字修飾就會解決該問題。
這個問題的源頭就在于可見性問題。為什么會出現這種問題呢?這里我們需要從CPU多核緩存架構講起。
線程,CPU多核緩存架構
一個雙核CPU架構可以如下圖所示:
首先需要明確的一點是,計算機實際上是分為多級緩存的,因為讀取緩存的數據性能十分快當CPU1需要讀取共享變量的值a時,首先會找緩存(即L1、L2、L3三級高速緩存),看看這個值是不是在L1。
很明顯,緩存沒辦法給CPU1它想要的數據,于是只能去主內存讀取共享變量的值
緩存得到共享變量的值之后,把數據交給寄存器,但是緩存留了個心眼,它把a的值存了起來,這樣下次別的線程再需要a的值時,就不用再去主內存問了
至此,一次完整的數據訪問流程走完了。L1和L2、L3都是高速緩存,從高速緩存和主內存讀取數據的速度完全是兩個概念。所以才會有主內存和緩存的設計。
java多線程。寫數據時刷新內存
針對上述模型,當CPU1讀取完數據后,假如對數據進行了修改,那么它會將緩存 —> 主內存的順序將修改后的數據刷新一遍,完成對數據的更新。
從讀到寫這一整個流程看起來似乎都是完美的,而且每次修改都把數據重新寫回到主內存,講道理不會有問題啊?
實際上問題正是出在這個看似完美的讀寫操作中:對于CPU1來說的確是完美的,但如果這時候CPU2來插一腳呢?我們思考下面這個流程:CPU1讀取數據a=1,CPU1的緩存中都有數據a的副本
CPU2也執行讀取操作,同樣CPU2也有數據a=1的副本
CPU1修改數據a=2,同時CPU1的緩存以及主內存a=2
java多線程volatile關鍵字的作用。CPU2再次讀取a,但是CPU2在緩存中命中數據,此時a=1
問題到這里已經很明顯了,CPU2并不知道CPU1改變了共享變量的值,因此造成了不可見問題。
緩存一致性協議
為了解決這個問題,在早期的CPU當中,是通過在總線上直接加鎖的形式來解決緩存不一致的問題。
但是正如Java中Synchronized一樣,直接加鎖太粗暴了,由于在鎖住總線期間,其他CPU無法訪問內存,導致效率低下。很明顯這樣做是不可取的。
所以就出現了緩存一致性協議。緩存一致性協議有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等。
java volatile關鍵字。MESI協議
最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。Modify(修改):當緩存行中的數據被修改時,該緩存行置為M狀態
Exclusive(獨占):當只有一個緩存行使用某個數據時,置為E狀態
Shared(共享):當其他CPU中也讀取某數據到緩存行時,所有持有該數據的緩存行置為S狀態
Invalid(無效):當某個緩存行數據修改時,其他持有該數據的緩存行置為I狀態
它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置為無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那么它就會從內存重新讀取。
java volatile原理?而這其中,監聽和通知又基于總線嗅探機制來完成。
總線嗅探機制
嗅探機制其實就是一個監聽器,回到我們剛才的流程,如果是加入MESI緩存一致性協議和總線嗅探機制之后:CPU1讀取數據a=1,CPU1的緩存中都有數據a的副本,該緩存行置為(E)狀態
CPU2也執行讀取操作,同樣CPU2也有數據a=1的副本,此時總線嗅探到CPU1也有該數據,則CPU1、CPU2兩個緩存行都置為(S)狀態
CPU1修改數據a=2,CPU1的緩存以及主內存a=2,同時CPU1的緩存行置為(S)狀態,總線發出通知,CPU2的緩存行置為(I)狀態
CPU2再次讀取a,雖然CPU2在緩存中命中數據a=1,但是發現狀態為(I),因此直接丟棄該數據,去主內存獲取最新數據
c++ volatile。當我們使用volatile關鍵字修飾某個變量之后,就相當于告訴CPU:我這個變量需要使用MESI和總線嗅探機制處理。從而也就保證了可見性。
指令重排序
在加入MESI和總線嗅探機制后,當CPU2發現當前緩存行數據無效時,會丟棄該數據,并前往主內存獲取最新數據。
但是這里又會產生一個問題:CPU1把數據刷回主內存是需要時間的,假如CPU2在主內存拿數據時,CPU1還沒有把數據刷回來呢?
很明顯,CPU2不會把資源浪費在這里傻等。它會先跳過和該數據有關的語句,繼續處理后面的邏輯。
比如說如下代碼:
java中volatile的作用。a = 1;
b = 2;
b++;
假如第一條語句需要等待CPU1數據刷新,那么CPU2可能就會先回來執行后面兩條語句。因為對于CPU2來說,先執行后面兩條語句不會對最終結果造成任何影響。
但是多線程環境下就會出現問題。關于指令重排序,我們放到內存屏障來講。
一些可能讓你困惑的問題
java線程同步、依舊是一開始的代碼,假如我們把TI線程循環的內容改成如下:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (a == 0) {
System.out.println(a);
java線程的生命周期?}
System.out.println("T1得知a = 1");
}
});
或者如下:
Thread t1 = new Thread(new Runnable() {
java序列化?@Override
public void run() {
while (a == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
java lambda,e.printStackTrace();
}
}
System.out.println("T1得知a = 1");
}
});
此時變量a沒有使用volatile修飾。
但是運行結果會讓你匪夷所思:程序正常結束,a變量對T1居然可見了!
while在作怪?
這是為什么呢?難道是因為在while循環中加了代碼導致的?
那我們加個變量b再來試試:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (a == 0) {
b++;
}
System.out.println("T1得知a = 1");
}
});
這次運行結果T1又沒辦法感知a的變化了,也就是說,并不是while中有代碼就會發生可見的現象。
那么真正的原因究竟是什么呢?
勤奮的CPU
這是一個很有趣的現象,有些人認為是因為println方法加了synchronized的原因。的確,鎖機制保證了每次執行都會把共享內存中的數據同步到工作內存中。
但Thread.sleep方法并沒有加呀?
真正的原因在于,CPU是很勤奮的,如果它發現自己有空閑的時間,就會主動去主內存里更新自己緩存中的數據。
而Thread.sleep方法對于CPU來說,會給它“喘息”的時間,讓它有空去把緩存里的數據去主內存刷新一下。
而后面的b++操作幾乎沒有給CPU任何機會休息,也就沒辦法去刷新緩存中的數據信息。
總結
事實上,我們的JMM模型就是類比CPU多核緩存架構的,它的作用是屏蔽掉了底層不同計算機的區別
JMM不是真實存在的,只是一個抽象的概念。volatile也是借助MESI緩存一致性協議和總線嗅探機制才得以完成
此外,當CPU不支持緩存一致性協議時,還是需要依靠總線加鎖的形式來保證線程安全
本文到這里就結束了,感謝大家看到最后,記得點贊加關注哦,如有不對之處還請多多指正。
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态