Java併發編程-可見性、原子性、有序性問題引入

這篇文章屬於讀書筆記,學習極客時間Java併發編程實戰課程時寫下的,部分內容來源於課程

可見性

由於存儲的成本和速度問題,我們的計算機採用了多級存儲。CPU集成的三級緩存,主內存以及我們常用的硬盤存儲。

我們的應用程序從硬盤存儲加載到主內存中,當我們的CPU去執行指令運算的時候,會把需要運算的代碼塊加載到CPU集成的緩存中。每一個CPU都有自己的緩存,所以當我們使用多線程併發編程時,就會出現可見性問題。

如圖所示:
極客時間-java併發編程-可見性問題
當我們的使用多線程計算count++的時候,假如現在變量count的值爲1。
線程A將變量count加載到CPU-1的緩存中,同時線程B也將變量count加載到CPU-2的緩存中。
他們同時計算count++,然後再將變量count寫入(同步)到主內存中,最後程序輸出變量count的值爲2,但是正確的兩次count++應該是3

這就是可見性問題

原子性

上文闡述的是因爲使用多個CPU完成多線程運算,實際上單個CPU也是可以完成多線程運算。
單個CPU是使用時間片、多線程分時複用實現的。

我們的應用程序在使用分時複用時,多個線程會同時操作一個內存區域,此時就會造成原子性問題。

如圖所示:
極客時間-java併發編程-原子性問題

還是當我們使用多線程計算count++的時候,實際上轉換成CPU指令會被拆解成三步,如下所示:

  1. 指令1:首先,需要把變量count從內存加載到 CPU的寄存器
  2. 指令2:之後,在寄存器中執行+1操作
  3. 指令3: 最後,將結果寫入到主內存(緩存機制可能會導致此處寫入到的是緩存而不是內存)

線程A在執行指令1之後,CPU時間片切換,執行線程B,此時線程B直接執行完三個指令,並寫入到內存(或緩存)中。此時緩存中已經是變量count已經是1了,但是由於線程A已經完成了指令1,在線程A的指令2中,變量count依然是0,所以線程A完成指令2、指令3以後,寫入內存(或緩存)中的變量count依然是1。而我們的期望值應該是2。

這就是原子性問題:CPU指令的原子操作

有序性

有序性問題主要是因爲編譯優化,我們寫的代碼邏輯可能與最後編譯後代碼邏輯不同。

比如

Object obj = new Object();

我們認爲的順序應該是

  1. 分配一塊內存M
  2. 在內存中初始化Object對象
  3. 然後M的地址賦值給obj變量。

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

  1. 分配一塊內存M
  2. 將M的地址賦值給obj變量;
  3. 最後在內存中M中初始化Object對象。

也就是說初始化對象的操作可能是在最後完成的,這樣會導致什麼問題呢?

極客時間-java併發編程-有序性問題

圖片是直接引用的,其中的instace等同於objSingleton等同於Object

在雙重校驗創建單例對象中,如果線程A完成了執行路徑中的第二步,但沒有完成執行路徑中的第三步。此時線程B進入雙重校驗時,變量obj不爲空,此時則會直接返回。當程序訪問變量obj的成員方法或者是成員變量時,就會出現NPE。

這就是有序性問題

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