java callable接口和runnable,java volatile線程可見_吃透Java并發:volatile是怎么保證可見性的

 2023-11-12 阅读 22 评论 0

摘要:前言volatile關鍵字能夠保證可見性和有序性,但是volatile為什么能夠保證可見性和有序性?為什么volatile又不能保證原子性?今天,我們從CPU多核緩存架構出發,結合MESI緩存一致性協議來深入剖析一下,volatile的原理。問題的出現我們先

前言

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不支持緩存一致性協議時,還是需要依靠總線加鎖的形式來保證線程安全

本文到這里就結束了,感謝大家看到最后,記得點贊加關注哦,如有不對之處還請多多指正。

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/172408.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息