溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java 高并發(fā)三 Java內(nèi)存模型和線程安全詳解

發(fā)布時間:2020-08-27 15:25:10 來源:網(wǎng)絡 閱讀:134 作者:loserone 欄目:編程語言

Java 高并發(fā)三  Java內(nèi)存模型和線程安全詳解
了解內(nèi)存模型中的一些特性
原子性
是指一個操作是不可中斷的。即使是在多個線程一起執(zhí)行的時候,一個操作一旦開始,就不會被其他線程干擾。一般認為CPU的指令都是原子操作,但是我們寫的代碼就不一定是原子操作了。
比如i++,這個操作不是原子操作,基本分為3個操作,讀取i,進行+1,賦值給i。
假設有兩個線程,當?shù)谝粋€線程讀取i=1時,還沒進行+1操作,切換到第二個線程,此時第二個線程也讀取的是i=1。隨后兩個線程進行后續(xù)+1操作,再賦值回去以后,i不是3,而是2。顯然數(shù)據(jù)出現(xiàn)了不一致性。

再比如在32位的JVM上面去讀取64位的long型數(shù)值,也不是一個原子操作。當然32位JVM讀取32位整數(shù)是一個原子操作。

有序性
在并發(fā)時,程序的執(zhí)行可能就會出現(xiàn)亂序。
計算機在執(zhí)行代碼時,不一定會按照程序的順序來執(zhí)行。
class OrderExample {
int a = 0;
boolean flag = false;
public void writer()
{
a = 1;
flag = true;
}
public void reader()
{
if (flag)
{
int i = a +1;
}
}
}
兩個方法分別被兩個線程調(diào)用。按照常理,寫線程應該先執(zhí)行a=1,再執(zhí)行flag=true。當讀線程進行讀的時候,i=2;
但是因為a=1和flag=true,并沒有邏輯上的關(guān)聯(lián)。所以有可能執(zhí)行的順序顛倒,有可能先執(zhí)行flag=true,再執(zhí)行a=1。這時當flag=true時,切換到讀線程,此時a=1還沒有執(zhí)行,那么讀線程將i=1。

當然這個不是絕對的。是有可能會發(fā)生亂序,有可能不發(fā)生。

那么為什么會發(fā)生亂序呢?這個要從cpu指令說起,Java中的代碼被編譯以后,最后也是轉(zhuǎn)換成匯編碼的。

一條指令的執(zhí)行是可以分為很多步驟的,假設cpu指令分為以下幾步
取指 IF
譯碼和取寄存器操作數(shù) ID
執(zhí)行或者有效地址計算 EX
存儲器訪問 MEM
寫回 WB
假設這里有兩條指令
Java 高并發(fā)三  Java內(nèi)存模型和線程安全詳解
一般來說我們會認為指令是串行執(zhí)行的,先執(zhí)行指令1,然后再執(zhí)行指令2。假設每個步驟需要消耗1個cpu時間周期,那么執(zhí)行這兩個指令需要消耗10個cpu時間周期,這樣做效率太低。事實上指令都是并行執(zhí)行的,當然在第一條指令在執(zhí)行IF的時候,第二條指令是不能進行IF的,因為指令寄存器等不能被同時占用。所以就如上圖所示,兩條指令是一種相對錯開的方式并行執(zhí)行。當指令1執(zhí)行ID的時候,指令2執(zhí)行IF。這樣只用6個cpu時間周期就執(zhí)行了兩個指令,效率比較高。

按照這個思路我們來看下A=B+C的指令是如何執(zhí)行的。
Java 高并發(fā)三  Java內(nèi)存模型和線程安全詳解
如圖所示,ADD操作時有一個空閑(X)操作,因為當想讓B和C相加的時候,在圖中ADD的X操作時,C還沒從內(nèi)存中讀?。ó擬EM操作完成時,C才從內(nèi)存中讀取。這里會有一個疑問,此時還沒有回寫(WB)到R2中,怎么會將R1與R1相加。那是因為在硬件電路當中,會使用一種叫“旁路”的技術(shù)直接把數(shù)據(jù)從硬件當中讀取出來,所以不需要等待WB執(zhí)行完才進行ADD)。所以ADD操作中會有一個空閑(X)時間。在SW操作中,因為EX指令不能和ADD的EX指令同時進行,所以也會有一個空閑(X)時間。

接下來舉個稍微復雜點的例子

a=b+c
d=e-f
對應的指令如下圖
Java 高并發(fā)三  Java內(nèi)存模型和線程安全詳解
原因和上面的類似,這里就不分析了。我們發(fā)現(xiàn),這里的X很多,浪費的時間周期很多,性能也被影響。有沒有辦法使X的數(shù)量減少呢?

我們希望用一些操作把X的空閑時間填充掉,因為ADD與上面的指令有數(shù)據(jù)依賴,我們希望用一些沒有數(shù)據(jù)依賴的指令去填充掉這些因為數(shù)據(jù)依賴而產(chǎn)生的空閑時間。
Java 高并發(fā)三  Java內(nèi)存模型和線程安全詳解
我們將指令的順序進行了改變
Java 高并發(fā)三  Java內(nèi)存模型和線程安全詳解
改變了指令順序以后,X被消除了??傮w的運行時間周期也減少了。

指令重排可以使流水線更加順暢

當然指令重排的原則是不能破壞串行程序的語義,例如a=1,b=a+1,這種指令就不會重排了,因為重排的串行結(jié)果和原先的不同。

指令重排只是編譯器或者CPU的優(yōu)化一種方式,而這種優(yōu)化就造成了本章一開始程序的問題。

如何解決呢?用volatile關(guān)鍵字,這個后面的系列會介紹到。

可見性
可見性是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。
可見性問題可能有各個環(huán)節(jié)產(chǎn)生。比如剛剛說的指令重排也會產(chǎn)生可見性問題,另外在編譯器的優(yōu)化或者某些硬件的優(yōu)化都會產(chǎn)生可見性問題。

比如某個線程將一個共享值優(yōu)化到了內(nèi)存中,而另一個線程將這個共享值優(yōu)化到了緩存中,當修改內(nèi)存中值的時候,緩存中的值是不知道這個修改的。

比如有些硬件優(yōu)化,程序在對同一個地址進行多次寫時,它會認為是沒有必要的,只保留最后一次寫,那么之前寫的數(shù)據(jù)在其他線程中就不可見了。

總之,可見性的問題大多都源于優(yōu)化。

接下來看一個Java虛擬機層面產(chǎn)生的可見性問題
package edu.hushi.jvm;

/*

  • @author -10
  • */
    public class VisibilityTest extends Thread {

    private boolean stop;

    public void run() {
    int i = 0;
    while(!stop) {
    i++;
    }
    System.out.println("finish loop,i=" + i);
    }

    public void stopIt() {
    stop = true;
    }

    public boolean getStop(){
    return stop;
    }
    public static void main(String[] args) throws Exception {
    VisibilityTest v = new VisibilityTest();
    v.start();

    Thread.sleep(1000);
    v.stopIt();
    Thread.sleep(2000);
    System.out.println("finish main");
    System.out.println(v.getStop());
    }

}
代碼很簡單,v線程一直不斷的在while循環(huán)中i++,直到主線程調(diào)用stop方法,改變了v線程中的stop變量的值使循環(huán)停止。
看似簡單的代碼運行時就會出現(xiàn)問題。這個程序在 client 模式下是能停止線程做自增操作的,但是在 server 模式先將是無限循環(huán)。(server模式下JVM優(yōu)化更多)

64位的系統(tǒng)上面大多都是server模式,在server模式下運行:

finish main
true
只會打印出這兩句話,而不會打印出finish loop??墒悄軌虬l(fā)現(xiàn)stop的值已經(jīng)是true了。
用工具將程序還原為匯編代碼
Java 高并發(fā)三  Java內(nèi)存模型和線程安全詳解

這里只截取了一部分匯編代碼,紅色部分為循環(huán)部分,可以清楚得看到只有在0x0193bf9d才進行了stop的驗證,而紅色部分并沒有取stop的值,所以才進行了無限循環(huán)。

這是JVM優(yōu)化后的結(jié)果。如何避免呢?和指令重排一樣,用volatile關(guān)鍵字。

如果加入了volatile,再還原為匯編代碼就會發(fā)現(xiàn),每次循環(huán)都會get一下stop的值。

Happen-Before
程序順序原則:一個線程內(nèi)保證語義的串行性
volatile規(guī)則:volatile變量的寫,先發(fā)生于讀,這保證了volatile變量的可見性
鎖規(guī)則:解鎖(unlock)必然發(fā)生在隨后的加鎖(lock)前
傳遞性:A先于B,B先于C,那么A必然先于C
線程的start()方法先于它的每一個動作
線程的所有操作先于線程的終結(jié)(Thread.join())
線程的中斷(interrupt())先于被中斷線程的代碼
對象的構(gòu)造函數(shù)執(zhí)行結(jié)束先于finalize()方法
這些原則保證了重排的語義是一致的。

線程安全的概念
指某個函數(shù)、函數(shù)庫在多線程環(huán)境中被調(diào)用時,能夠正確地處理各個線程的局部變量,使程序功能正確完成。

比如最開始所說的i++的例子
Java 高并發(fā)三  Java內(nèi)存模型和線程安全詳解
就會導致線程不安全。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI