深入理解Java內(nèi)存模型的語(yǔ)義

image

前言

Java內(nèi)存模型(JMM)給我們介紹了在當(dāng)代不同的硬件架構(gòu)情況下,多線程程序需要關(guān)注什么問(wèn)題以及如何利用JMM來(lái)正確的處理這些問(wèn)題。

多線程帶來(lái)的問(wèn)題

多線程程序主要關(guān)注兩個(gè)問(wèn)題:

(1)共享變量可見(jiàn)性問(wèn)題

(2)代碼重排序一致性問(wèn)題

Java內(nèi)存模型的關(guān)鍵點(diǎn)

JMM已經(jīng)保證了as-if-serial原則,也就是Java的程序在單線程情況下,不管JIT做不做重排序,也不管代碼指令在幾個(gè)CPU上執(zhí)行,看到的最終結(jié)果必須和代碼順序執(zhí)行的結(jié)果保持一致。

但是在多線程的情況下,如何才能正確的處理的變量可見(jiàn)性問(wèn)題和重排序的一致性問(wèn)題?

關(guān)鍵在于理解和運(yùn)用下面的兩塊內(nèi)容:

(1)happens-before相關(guān)

(2)data race相關(guān)

關(guān)于Memory Consistency Errors

Memory Consistency Errors中文含義是:內(nèi)存一致性錯(cuò)誤,指的的是多線程環(huán)境下,對(duì)于同一個(gè)共享變量的值在不同的線程看到的視圖不一致。

偽代碼如下:

int counter = 0;

此時(shí)A線程正在執(zhí)行:

counter++;

然后過(guò)了幾秒后,B線程打印這個(gè)值:

System.out.println(counter);

此時(shí)B線程的打印結(jié)果很大可能是0,但A線程里面其實(shí)這個(gè)值已經(jīng)是1了,這就是典型的內(nèi)存一致性錯(cuò)誤。這情況種只能通過(guò)happens-before規(guī)則來(lái)避免。

關(guān)于happens-before

happens-before是JMM里面保證在一個(gè)線程里面執(zhí)行的action(讀或者寫(xiě))的結(jié)果,可以在隨后的其他線程里面立馬可見(jiàn)的一系列規(guī)則。比如 x happens-before y ,那么不管x和y是不是在同一個(gè)線程里面,JMM都會(huì)保證對(duì)于x的update都會(huì)立馬里面對(duì)y線程可見(jiàn),也就是x總會(huì)先于y執(zhí)行,前提是兩者必須有happens-before關(guān)系,否則就會(huì)出現(xiàn)上面的內(nèi)存一致性錯(cuò)誤的問(wèn)題。

如何建立happens-before關(guān)系? 這里面有幾條規(guī)則:

(1) 單線程中的程序執(zhí)行結(jié)果與代碼的順序執(zhí)行結(jié)果保持一致。

你能會(huì)好奇,難道單線程不是順序執(zhí)行的嗎? 答案是的確不一定按照順序執(zhí)行,這個(gè)跟硬件的指令重排序有關(guān),目的是為了優(yōu)化性能讓cpu更快的執(zhí)行指令,但有happens-before保證,所以結(jié)果跟代碼順序執(zhí)行的結(jié)果保持一致,這是最基礎(chǔ)的保證,也是最重要的保證。

(2)同一個(gè)鎖的unlock操作,在其他線程lock后,變量是可見(jiàn)的。

class LockRule {
    private int value = 0;
    
    public synchronized void setValue(int value) {
        this.value = value;
    }
    
    public synchronized int getValue() {
        return value;
    }
}

也就是在A線程中執(zhí)行setValue操作,在B線程中執(zhí)行g(shù)etValue方法是可以看到變化的,注意這里一定是同一個(gè)監(jiān)視器才可以,比如上面這段代碼就是用對(duì)象做為監(jiān)視器。此外ReentrantLock鎖也具有相同的語(yǔ)義。

(3)volatile修飾的變量,在一個(gè)線程update后,立刻對(duì)其他的線程可見(jiàn)。這個(gè)不多說(shuō),前面的文章介紹過(guò)。

(4)關(guān)于Thread的start方法,是指在一個(gè)線程A中啟動(dòng)另外另外一個(gè)線程B時(shí),A里面所有的變量對(duì)B是可見(jiàn)的,最常見(jiàn)的就是我們?cè)趈ava的main線程中啟動(dòng)的線程是可以看到啟動(dòng)之前所有的main線程的變量的。底層是啟動(dòng)前把所有內(nèi)容都同步到主內(nèi)存里面了,然后新的線程會(huì)從主內(nèi)存里面拷貝一份數(shù)據(jù)到自己的cache,所以是可見(jiàn)的。

(5)關(guān)于Thread的join方法,同樣道理,比如我在java的main線程里面聲明了一個(gè)線程B,然后調(diào)用
B.start() //啟動(dòng)B線程
B.join() //main線程等待B線程結(jié)束
此時(shí)在B線程里面修改了成員變量,在B線程結(jié)束的時(shí)候,main線程是可以直接看到最終變化的。這是一個(gè)線程結(jié)束的時(shí)候會(huì)把自己緩存的值給刷新到主內(nèi)存,所以感知了B線程結(jié)束的主線程是可以看到所有變化的。

(6)關(guān)于Thread的interrupt方法,同樣道理,在java的main線程中,比如我在java的main線程里面聲明了一個(gè)線程B,然后調(diào)用
B.start() //啟動(dòng)B線程
B.interrupt() //打斷B線程,此時(shí)B線程的是可以看到主線程的修改的狀態(tài)

(7)對(duì)于實(shí)例的finalize()方法,當(dāng)實(shí)例的構(gòu)造方法執(zhí)行完畢之后,如果再執(zhí)行finalize()方法,此時(shí)實(shí)例里面的所有變量不管有多少線程修改過(guò)對(duì)finalize()方法都是可見(jiàn)的。

(8)傳遞性規(guī)則: 如果 A happens-before B 并且 B happens-before C, 那么 A happens-before C

關(guān)于data race

data race又叫數(shù)據(jù)競(jìng)爭(zhēng),在這里指的多個(gè)線程之間沒(méi)有符合的happens-before規(guī)則,但是它們又需要修改同一個(gè)共享變量,比如上面的counter的例子,最終會(huì)造成內(nèi)存一致性的問(wèn)題,這種情況下可以通過(guò)Java自帶的一些鎖機(jī)制來(lái)避免。

關(guān)于上篇文章遺留問(wèn)題

在上篇文章中,我遺留了一個(gè)問(wèn)題,那就在下面的代碼中:

    private  static boolean  keepRunning=true;

    public static void main(String[] args)  throws Exception {

        new Thread(
                ()->{

                    while (keepRunning){
                        //System.out.println();
                    }
                }

        ).start();

        Thread.sleep(1000);
        keepRunning=false;

    }

如果我把while循環(huán)里面的打印語(yǔ)句去掉,那么即使沒(méi)有volatile關(guān)鍵字,程序也可以結(jié)束循環(huán),為什么? 其實(shí)答案就在今天的知識(shí)里面,因?yàn)榇蛴≌Z(yǔ)句會(huì)鎖住當(dāng)前的實(shí)例,源碼如下:

    public void println(boolean x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

對(duì)應(yīng)到上面的happens-before的第二條規(guī)則就很容易的解釋通了。

總結(jié)

本篇文章主要介紹了Java內(nèi)存模型主要描述的問(wèn)題以及解決多線程環(huán)境下的問(wèn)題思路,我們了解和學(xué)習(xí)了什么是內(nèi)存一致性錯(cuò)誤,happens-before的規(guī)則,數(shù)據(jù)競(jìng)爭(zhēng)的內(nèi)容,掌握了這些知識(shí)將非常有助于我們深入到Java并發(fā)編程的世界,希望大家可以有所收獲。

?著作權(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)容