天天看點

Java并發程式設計實戰 01并發程式設計的Bug源頭

Java并發程式設計實戰 01并發程式設計的Bug源頭

摘要#

編寫正确的并發程式對我來說是一件極其困難的事情,由于知識不足,隻知道synchronized這個修飾符進行同步。

本文為學習極客時間:Java并發程式設計實戰 01的總結,文章取圖也是來自于該文章

并發Bug源頭#

在計算機系統中,程式的執行速度為:CPU > 記憶體 > I/O裝置 ,為了平衡這三者的速度差異,計算機體系機構、作業系統、編譯程式都進行了優化:

1.CPU增加了緩存,以均衡和記憶體的速度差異

2.作業系統增加了程序、線程,已分時複用CPU,以均衡 CPU 與 I/O 裝置的速度差異

3.編譯程式優化指令執行順序,使得緩存能夠更加合理的利用。

但是這三者導緻的問題為:可見性、原子性、有序性

源頭之一:CPU緩存導緻的可見性問題#

一個線程對共享變量的修改,另外一個線程能夠立即看到,那麼就稱為可見性。

現在多核CPU時代中,每顆CPU都有自己的緩存,CPU之間并不會共享緩存;

如線程A從記憶體讀取變量V到CPU-1,操作完成後儲存在CPU-1緩存中,還未寫到記憶體中。

此時線程B從記憶體讀取變量V到CPU-2中,而CPU-1緩存中的變量V對線程B是不可見的

當線程A把更新後的變量V寫到記憶體中時,線程B才可以從記憶體中讀取到最新變量V的值

上述過程就是線程A修改變量V後,對線程B不可見,那麼就稱為可見性問題。

源頭之二:線程切換帶來的原子性問題#

現代的作業系統都是基于線程來排程的,現在提到的“任務切換”都是指“線程切換”

Java并發程式都是基于多線程的,自然也會涉及到任務切換,在進階語言中,一條語句可能就需要多條CPU指令完成,例如在代碼 count += 1 中,至少需要三條CPU指令。

指令1:把變量 count 從記憶體加載到CPU的寄存器中

指令2:在寄存器中把變量 count + 1

指令3:把變量 count 寫入到記憶體(緩存機制導緻可能寫入的是CPU緩存而不是記憶體)

作業系統做任務切換,可以發生在任何一條CPU指令執行完,是以并不是進階語言中的一條語句,不要被 count += 1 這個操作蒙蔽了雙眼。假設count = 0,線程A執行完 指令1 後 ,做任務切換到線程B執行了 指令1、指令2、指令3後,再做任務切換回線程A。我們會發現雖然兩個線程都執行了 count += 1 操作。但是得到的結果并不是2,而是1。

如果 count += 1 是一個不可分割的整體,線程的切換可以發生在 count += 1 之前或之後,但是不會發生在中間,就像個原子一樣。我們把一個或者多個操作在 CPU 執行的過程中不被中斷的特性稱為原子性

源頭之三:編譯優化帶來的有序性問題#

有序性指的是程式按照代碼的先後順序執行。編譯器為了優化性能,可能會改變程式中的語句執行先後順序。如:a = 1; b = 2;,編譯器可能會優化成:b = 2; a = 1。在這個例子中,編譯器優化了程式的執行先後順序,并不影響結果。但是有時候優化後會導緻意想不到的Bug。

在單例模式的雙重檢查建立單例對象中。如下代碼:

Copy

public class Singleton {

private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}           

}

問題出現在了new Singletion()這行代碼,我們以為的執行順序應該是這樣的:

指令1:配置設定一塊記憶體M

指令2:在記憶體M中執行個體化Singleton對象

指令3:instance變量指向記憶體位址M

但是實際優化後的執行路徑确實這樣的:

指令2:instance變量指向記憶體位址M

指令3:在記憶體M中執行個體化Singleton對象

這樣的話看出來什麼問題了嗎?當線程A執行完了指令2後,切換到了線程B,

線程B判斷到 if (instance != null)。直接傳回instance,但是此時的instance還是沒有被執行個體化的啊!是以這時候我們使用instance可能就會觸發空指針異常了。如圖:

總結#

在寫并發程式的時候,需要時刻注意可見性、原子性、有序性的問題。在深刻了解這三個問題後,寫起并發程式也會少一點Bug啦~。記住了下面這段話:CPU緩存會帶來可見性問題、線程切換帶來的原子性問題、編譯優化帶來的有序性問題。

參考文章:極客時間:Java并發程式設計實戰 01 | 可見性、原子性和有序性問題:并發程式設計Bug的源頭

如果我的文章幫助到您,可以關注我的微信公衆号,第一時間分享文章給您

作者: Johnson木木

出處:

https://www.cnblogs.com/Johnson-lin/p/12697533.html