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

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

指令1:分配一塊內存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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章