多線程積累:JMM模型

(一)前言

學(xué)習(xí)多線程,要理解java內(nèi)存模型,才能理解多線程情況下,數(shù)據(jù)的變化,指令的運(yùn)行等,才能更好的了解多線程的運(yùn)行情況和日常使用的注意點(diǎn)。

(二)JMM與硬件內(nèi)存結(jié)構(gòu)

java內(nèi)存模型與硬件內(nèi)存結(jié)構(gòu).png

如上圖所示,可以看到JMM的大概結(jié)構(gòu)與硬件內(nèi)存結(jié)構(gòu)之間的關(guān)系,每個(gè)線程只能訪問(wèn)自己工作內(nèi)存的數(shù)據(jù),工作內(nèi)存中存儲(chǔ)著主內(nèi)存中變量復(fù)制的副本,這兩個(gè)內(nèi)存的數(shù)據(jù)可以存儲(chǔ)在硬件內(nèi)存中的任一地方,并沒(méi)有特殊劃分。
JMM只是一種抽象的概念,是一種規(guī)則,并不真實(shí)存在,對(duì)于計(jì)算機(jī)而言,并不劃分工作內(nèi)存和主內(nèi)存,而是都存儲(chǔ)在計(jì)算機(jī)主內(nèi)存中。

(三)JMM的三種特性

1.原子性

在多線程環(huán)境下,一個(gè)操作一旦開(kāi)始就不會(huì)被其他線程影響。
比如一個(gè)靜態(tài)變量,被兩個(gè)線程同時(shí)進(jìn)行操作,無(wú)論如何運(yùn)行,最后的結(jié)構(gòu)必定是兩個(gè)線程中的一種結(jié)果。
特例:32位的系統(tǒng),如果操作long或者double,由于操作位數(shù)問(wèn)題,最終的結(jié)果可能并不是兩個(gè)線程中的任一結(jié)果。
其實(shí),在上述描述中,有一點(diǎn)無(wú)論如何運(yùn)行,在計(jì)算機(jī)執(zhí)行程序中,為了提高性能,編譯器和處理器會(huì)對(duì)指令進(jìn)行重排。

指令重排
  • (1)編譯器重排
    簡(jiǎn)單的舉個(gè)例子:
    主線程:
d=3;
c=3;

線程A:

a=c;
d=1;

線程B:

b=d;
c=2;

在以上兩個(gè)線程之前,對(duì)c和d進(jìn)行賦值,從程序的執(zhí)行順序來(lái)說(shuō),似乎不可能存在a=2,b=1的情況,但是指令重排之后,可能存在:
線程A:

d=1;
a=c;

線程B:

c=2;
b=d;

此時(shí),看起來(lái)就更可能存在a=2,b=1的情況,所以,多線程情況下,對(duì)變量能否保持一致是不可預(yù)知的。

  • (2)處理器重排
    簡(jiǎn)單舉個(gè)例子:
a=b+c;
d=a-e

在上述代碼里面,落實(shí)到指令可以理解為:

  • 1.把b的值加載到寄存器
  • 2.把c的值加載到寄存器
  • 3.將b和c相加得到a
  • 4.將a加載到寄存器
  • 5.把e的值加載到寄存器
  • 6.將a減e得到d
  • 7.將d加載到寄存器。

其實(shí)上面的指令有個(gè)優(yōu)化的點(diǎn),就是將步驟5提前到2之后,因?yàn)椴襟E3和4都需要前面數(shù)據(jù)準(zhǔn)備好之后才能進(jìn)行,所以會(huì)進(jìn)行中斷,此時(shí)中斷,會(huì)影響5的運(yùn)行,將5提前,可以提高CPU的性能。
重排保證了串行語(yǔ)義的執(zhí)行,但是在多線程的環(huán)境下,這樣是毀滅性的,導(dǎo)致結(jié)果的不可預(yù)知性。

如下代碼:

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }

    public void read(){
        if(flag){
            int i = a + 1;
        }
    }
}

在單線程的場(chǎng)景下,先調(diào)用writer(),再次調(diào)用read(),得到的結(jié)果是i=2。
在多線程的場(chǎng)景下,指令重排之后,read()方法在讀到flagtrue的情況下,可能誤讀a=0,此時(shí)得到的結(jié)果為i=1

2.有序性

有序性是指在單線程的執(zhí)行代碼,我們可以認(rèn)為代碼的執(zhí)行是按照順序執(zhí)行的,但是在多線程場(chǎng)景下,因?yàn)橹噶钪嘏?,?dǎo)致最終的指令可能是亂序的,在本線程內(nèi),所有操作都視為有序的,但是多線程下,存在共享變量,一個(gè)線程需要觀察另一個(gè)線程,所以操作都是無(wú)序的。

3.可見(jiàn)性

可見(jiàn)性指的是當(dāng)一個(gè)線程修改了某個(gè)共享變量的值,其他線程是否能夠馬上得知這個(gè)修改的值。這個(gè)概念僅代表在并發(fā)程序上的概念。由于每個(gè)線程會(huì)將共享變量拷貝到自己的工作線程中,由于指令重排的情況,也會(huì)存在可見(jiàn)性的問(wèn)題,導(dǎo)致結(jié)果不是預(yù)期的結(jié)果。

(四)JMM提供的解決方案

針對(duì)以上的三種特性在多線程環(huán)境下的問(wèn)題,JMM提供了相應(yīng)的解決方案。

  • 原子性問(wèn)題
    除了JVM自身提供的對(duì)基本數(shù)據(jù)類(lèi)型讀寫(xiě)操作的原子性外,對(duì)于方法級(jí)別或者代碼塊級(jí)別的原子性操作,可以使用synchronized關(guān)鍵字或者重入鎖(ReentrantLock)保證程序執(zhí)行的原子性。
  • 可見(jiàn)性問(wèn)題
    可見(jiàn)性問(wèn)題,可以使用synchronized關(guān)鍵字或者volatile關(guān)鍵字解決,它們都可以使一個(gè)線程修改后的變量立即對(duì)其他線程可見(jiàn)。
  • 有序性問(wèn)題
    對(duì)于指令重排導(dǎo)致的可見(jiàn)性問(wèn)題和有序性問(wèn)題,則可以利用volatile關(guān)鍵字解決,因?yàn)関olatile的另外一個(gè)作用就是禁止重排序優(yōu)化,關(guān)于volatile稍后會(huì)進(jìn)一步分析。

同時(shí),JMM內(nèi)部還定義一套happens-before 原則來(lái)保證多線程環(huán)境下兩個(gè)操作間的原子性、可見(jiàn)性以及有序性。

happens-before 原則

  • 1.程序順序原則
    即在一個(gè)線程內(nèi)必須保證語(yǔ)義串行性,也就是說(shuō)按照代碼順序執(zhí)行。
  • 2.鎖規(guī)則
    解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個(gè)鎖的加鎖(lock)之前,也就是說(shuō),如果對(duì)于一個(gè)鎖解鎖后,再加鎖,那么加鎖的動(dòng)作必須在解鎖動(dòng)作之后(同一個(gè)鎖)。
  • 3.volatile規(guī)則
    volatile變量的寫(xiě),先發(fā)生于讀,這保證了volatile變量的可見(jiàn)性,簡(jiǎn)單的理解就是,volatile變量在每次被線程訪問(wèn)時(shí),都強(qiáng)迫從主內(nèi)存中讀該變量的值,而當(dāng)該變量發(fā)生變化時(shí),又會(huì)強(qiáng)迫將最新的值刷新到主內(nèi)存,任何時(shí)刻,不同的線程總是能夠看到該變量的最新值。
  • 4.線程啟動(dòng)規(guī)則
    線程的start()方法先于它的每一個(gè)動(dòng)作,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值,那么當(dāng)線程B執(zhí)行start方法時(shí),線程A對(duì)共享變量的修改對(duì)線程B可見(jiàn)。
  • 5.傳遞性
    A先于B ,B先于C 那么A必然先于C
  • 6.線程終止規(guī)則
    線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當(dāng)前執(zhí)行的線程終止。假設(shè)在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對(duì)共享變量的修改將對(duì)線程A可見(jiàn)。
  • 7.線程中斷規(guī)則
    對(duì)線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過(guò)Thread.interrupted()方法檢測(cè)線程是否中斷。
  • 8.對(duì)象終結(jié)規(guī)則
    對(duì)象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法

(五)volatile

volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制。volatile關(guān)鍵字有如下兩個(gè)作用:

  • 保證被volatile修飾的共享變量對(duì)所有線程總數(shù)可見(jiàn)的,也就是當(dāng)一個(gè)線程修改了一個(gè)被volatile修飾共享變量的值,新值總數(shù)可以被其他線程立即得知。
  • 禁止指令重排序優(yōu)化。

volatile的可見(jiàn)性

關(guān)于volatile的可見(jiàn)性作用,我們必須意識(shí)到被volatile修飾的變量對(duì)所有線程總數(shù)立即可見(jiàn)的,對(duì)volatile變量的所有寫(xiě)操作總是能立刻反應(yīng)到其他線程中,但是對(duì)于volatile變量運(yùn)算操作在多線程環(huán)境并不保證安全性。

volatile禁止重排優(yōu)化

禁止重排其實(shí)在單例模式中已經(jīng)有提現(xiàn),就是單例模式中的雙重校驗(yàn)鎖模式。
instance = new Singleton();偽代碼如下:

memory = allocate(); //1.分配對(duì)象內(nèi)存空間
instance(memory);    //2.初始化對(duì)象
instance = memory;   //3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance!=null

如果去掉volatile,則可重排優(yōu)化為:

memory = allocate(); //1.分配對(duì)象內(nèi)存空間
instance = memory;   //3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance!=null,但是對(duì)象還沒(méi)有初始化完成!
instance(memory);    //2.初始化對(duì)象

以上可以發(fā)現(xiàn),當(dāng)一條線程訪問(wèn)instance不為null時(shí),由于instance實(shí)例未必已初始化完成,也就造成了線程安全問(wèn)題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容