Java多線程探索(一):從硬件看多線程併發安全問題的根源

一、併發安全根源

併發編程出現安全問題的原因無非三個:有序性、可見性、原子性。這三個問題在硬件中有其具體的產生原因。

1、有序性。

程序的一個要求就是:在編碼保證正確的前提下,執行結果必須是正確的,程序要得是硬件做正確的事,強調結果的準確性。但對於硬件來說,更高的性能是其孜孜不倦的目標,硬件追求的是正確的做事,強調過程的效率。兩者是否可以兼得?硬件是否可以既有效率又正確的完成程序任務呢?答案是肯定的,在軟硬件協調下可以保證儘快的正確的完成程序任務。

硬件對指令執行的優化:指令流水。

關於指令流水可以參考我的一片博客:指令流水簡介

當了解了什麼是指令流水、爲什麼指令流水後也就是明白了爲什麼硬件要進行指令重排,由於硬件只需要保證 As-If-Serial語義:不管怎麼重排序,單線程程序的執行結果不能被改變。一個很關鍵的字眼是單線程!!那多線程怎麼辦?

在上面指令流水簡介中你知道了硬件對於指令重排的最基本限制:結構相關、數據相關、控制相關。這三個相關限制將能夠保障As-If-Serial語義,對於多線程程序的的這三個相關性不在硬件的考慮範圍之內,需要程序自己進行有序性控制。

硬件提供內存屏障指令給程序以保障多線程的指令有序性。內存屏障指令通過禁止某些特殊場景的指令重排來控制多線程的有序性(當然內存屏障也用於保障可見性,下一個小結探索)。

JMM(Java內存模型)將內存屏障指令分爲四類,但是由於Java跨平臺的特性,不是所有的平臺都支持這四種指令:

  1. StoreStore屏障:不準將屏障前的數據存儲指令重排到屏障後的數據存儲指令後面。可以確保前一個指令存儲的數據的可見性(從緩存刷新到內存),同時保證後一個指令的數據不會被前一個指令覆蓋,也就是在指令流水裏面數據相關的寫後寫問題。
  2. StoreLoad屏障:不準將屏障前的數據存儲指令重排到屏障後的數據加載指令後面。可以確保前一個指令存儲的數據的可見性(從緩存刷新到內存),同時保證後面的數據加載指令不會加載到一箇舊值,也就是在指令流水裏面數據相關的寫後讀問題。
  3. LoadLoad屏障:不準將屏障前的數據加載指令重排到屏障後的數據加載指令後面。指令流水的數據相關中也沒有讀後讀問題,該屏障用於解決特殊讀和普通讀的重排序問題(比如volatile變量的讀和普通變量的讀)。
  4. LoadStore屏障:不準將屏障前的數據加載指令重排到屏障後的數據存儲指令後面。可以確保前一個指令不會加載到後面存儲指令存儲的新的值,也就是在指令流水裏面數據相關的讀後寫問題。

注意:

  1. 上面的屏障指令類是JMM的分類,不是具體硬件的具體指令。
  2. 只有對同一個數據操作的時候纔會出現這些情況,然而Java是以共享內存來進行線程間的消息傳遞的,所以多線程對同一個數據的操作司空見慣。
  3. 可以發現內存屏障的目的往往都和數據可見性相關,因爲三個原因密不可分。
  4. Java編譯器在編譯的時候通過插入平臺相關的內存屏障指令來保證多線程的併發安全,前提是正確的併發控制。

總結一下有序性:由於硬件指令重排的情況下,指令在執行的時候是亂序執行的,但是如果是單線程程序或者正確併發控制的多線程程序(正確的插入了內存屏障指令),硬件執行保障As-If-Serial語義,在保障正確結果的前提下儘量提高效率。所以可以又快又正確的執行程序。

二、可見性根源

上一節的內存屏障好像順便就解決了數據可見性的問題,但是指令重排的數據相關是指令之間的數據相關。現代計算機基本上都是多核,且都是有高速緩存的,如何保障各個核心緩存之間的數據一致性和可見性就是一個大問題。還好有緩存一致性協議。

MESI緩存一致性協議:將緩存行(緩存的最小單位,往往是多個數據)用MESI四種狀態進行標記,然後通過監聽事件來進行緩存行的狀態轉換。以此來保障各個緩存中的數據一致性和數據可見性。

MESI就是四種狀態的首字母縮寫:

  1. M(Modified) :這行緩存數據有效,但是緩存行數據被修改了,和內存中的數據不一致,數據只存在於本Cache中。
  2. E(Exclusive) :這行緩存數據有效,緩存行數據和內存中的數據一致,只存在於本Cache中。
  3. S(Shared) :這行緩存數據有效,緩存行數據和內存中的數據一致,存在於很多Cache中(也可能只有這個Cache有,只是它自己不知道而已)。
  4. I(Invalid): 這行緩存數據無效,不能夠使用,必須從內存重新加載。

CPU緩存一致性協議MESI這篇博客對緩存一致性協議有詳細的介紹。

在瞭解了緩存一致性協議後,發現由於緩存一致性協議的優化(存儲緩存和無效隊列),緩存數據的更新不是實時的,而是異步更新。這就導致了數據不一致問題,僅僅緩存一致性協議無法解決可見性和一致性問題。

內存屏障的另外一個作用:強制刷新緩存。內存屏障緩存刷新可以保證數據的可見性,但是一致性仍然無法保證。

總線事物:Lock信號可以鎖總線/緩存(也就是總線鎖和緩存鎖,導致其他CPU無法獲取總線,也就無法訪問內存,順便實現了原子性),在Lock總線總線/緩存後再執行指令,在釋放鎖的時候將數據寫入內存。

好了,現在Lock信號、內存屏障、緩存一致性協議三方合力,終於完美解決了可見性、順序性,還順便解決了原子性。更詳細的緩存可以看看這篇好博客:理解CPU高速緩存的工作原理

三、原子性根源

原子性就是指一個不可分割的部分,要麼全部,要麼沒有。

原子性的實現主要是依賴鎖。下一篇博客我準備來和你一起探索虛擬機的鎖機制。

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