資深架構師多年總結,帶你深入解析Java內存模型的語義

資深架構師多年總結,帶你深入解析Java內存模型的語義

鎮樓小姐姐

可獲得兩大新人禮包

36份一線互聯網Java面試電子書

84個Java稀缺面試題視頻


前言

Java內存模型(JMM)給我們介紹了在當代不同的硬件架構情況下,多線程程序需要關注什麼問題以及如何利用JMM來正確的處理這些問題。

多線程帶來的問題

多線程程序主要關注兩個問題:

(1)共享變量可見性問題

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

Java內存模型的關鍵點

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

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

關鍵在於理解和運用下面的兩塊內容:

(1)happens-before相關

(2)data race相關

關於Memory Consistency Errors

Memory Consistency Errors中文含義是:**內存一致性錯誤**,指的的是多線程環境下,對於同一個共享變量的值在不同的線程看到的視圖不一致。

偽代碼如下:

Java代碼

```

int counter = 0;

```

此時A線程正在執行:

Java代碼

```

counter++;

```

然後過了幾秒後,B線程打印這個值:

Java代碼

```

System.out.println(counter);

```

此時B線程的打印結果很大可能是0,但A線程裡面其實這個值已經是1了,這就是典型的內存一致性錯誤。這情況種只能通過happens-before規則來避免。

關於happens-before

happens-before是JMM裡面保證在一個線程裡面執行的action(讀或者寫)的結果,可以在隨後的其他線程裡面立馬可見的一系列規則。比如 x happens-before y ,那麼不管x和y是不是在同一個線程裡面,JMM都會保證對於x的update都會立馬裡面對y線程可見,也就是x總會先於y執行,前提是兩者必須有happens-before關係,否則就會出現上面的內存一致性錯誤的問題。

如何建立happens-before關係? 這裡面有幾條規則:

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

你能會好奇,難道單線程不是順序執行的嗎? 答案是的確不一定按照順序執行,這個跟硬件的指令重排序有關,目的是為了優化性能讓cpu更快的執行指令,但有happens-before保證,所以結果跟代碼順序執行的結果保持一致,這是最基礎的保證,也是最重要的保證。

(2)同一個鎖的unlock操作,在其他線程lock後,變量是可見的。

Java代碼

```

class LockRule {

private int value = 0;

public synchronized void setValue(int value) {

this.value = value;

}

public synchronized int getValue() {

return value;

}

}

  1. ```

也就是在A線程中執行setValue操作,在B線程中執行getValue方法是可以看到變化的,注意這裡一定是同一個監視器才可以,比如上面這段代碼就是用對象做為監視器。此外ReentrantLock鎖也具有相同的語義。

(3)volatile修飾的變量,在一個線程update後,立刻對其他的線程可見。這個不多說,前面的文章介紹過。

(4)關於Thread的start方法,是指在一個線程A中啟動另外另外一個線程B時,A裡面所有的變量對B是可見的,最常見的就是我們在java的main線程中啟動的線程是可以看到啟動之前所有的main線程的變量的。底層是啟動前把所有內容都同步到主內存裡面了,然後新的線程會從主內存裡面拷貝一份數據到自己的cache,所以是可見的。

B.start() //啟動B線程

B.join() //main線程等待B線程結束

此時在B線程裡面修改了成員變量,在B線程結束的時候,main線程是可以直接看到最終變化的。這是一個線程結束的時候會把自己緩存的值給刷新到主內存,所以感知了B線程結束的主線程是可以看到所有變化的。

B.start() //啟動B線程

B.interrupt() //打斷B線程,此時B線程的是可以看到主線程的修改的狀態

(7)對於實例的finalize()方法,當實例的構造方法執行完畢之後,如果再執行finalize()方法,此時實例裡面的所有變量不管有多少線程修改過對finalize()方法都是可見的。

(8)傳遞性規則: 如果 A happens-before B 並且 B happens-before C, 那麼 A happens-before C

關於data race

data race又叫數據競爭,在這裡指的多個線程之間沒有符合的happens-before規則,但是它們又需要修改同一個共享變量,比如上面的counter的例子,最終會造成內存一致性的問題,這種情況下可以通過Java自帶的一些鎖機制來避免。

關於上篇文章遺留問題

在上篇文章中,我遺留了一個問題,那就在下面的代碼中:

Java代碼

```

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循環裡面的打印語句去掉,那麼即使沒有volatile關鍵字,程序也可以結束循環,為什麼? 其實答案就在今天的知識裡面,因為打印語句會鎖住當前的實例,源碼如下:

Java代碼

```

public void println(boolean x) {

synchronized (this) {

print(x);

newLine();

}

}

```

對應到上面的happens-before的第二條規則就很容易的解釋通了。

總結

本篇文章主要介紹了Java內存模型主要描述的問題以及解決多線程環境下的問題思路,我們瞭解和學習了什麼是內存一致性錯誤,happens-before的規則,數據競爭的內容,掌握了這些知識將非常有助於我們深入到Java併發編程的世界,希望大家可以有所收穫。


分享到:


相關文章: