Java內存模型簡介

Java 內存模型 (JMM)描述了線程在 Java 運行時環境中的行為。內存模型是 Java 語言語義的一部分,它描述了程序員在開發不是針對特定 Java 機器而是針對整個 Java 的軟件時可以和不應該期望的內容。

1995 年開發的原始 Java 內存模型(特別是指“percolocal 內存”)被認為是失敗的:許多優化無法在不失去代碼安全保證的情況下進行。特別是,編寫多線程“單”有幾種選擇:

  • 訪問單例的每個行為(即使對像是很久以前創建的,並且什麼都不能改變)都會導致線程間鎖定;
  • 或者在某種情況下,系統會發出未完成的獨行者;
  • 或者在某種情況下,系統會產生兩個獨行者;
  • 或者設計將取決於特定機器的行為。

因此,重新設計了內存機制。2005 年,隨著 Java 5 的發布,提出了一種新方法,並隨著 Java 14 的發布進一步改進。

新模型基於三個規則:

規則 #1:單線程程序偽順序運行。這意味著:實際上,處理器可以在每個時鐘執行多個操作,同時更改它們的順序,但是,所有數據依賴性仍然存在,因此該行為與順序操作沒有區別。

規則 2:沒有無處不在的值。讀取任何變量(非易失性 long 和 double 除外,此規則可能不適用)將返回默認值(零)或由另一個命令寫入的內容。

規則3:其餘事件按順序執行,如果它們通過嚴格的偏序關係“先執行”(先發生)連接。

發生在之前

Leslie Lamport之前提出了 Happens 的概念。這是在原子命令(++ 和 -- 不是原子的)之間引入的嚴格偏序關係,並不意味著“物理上先於”。

它說第二個團隊將“知道”第一個團隊所做的更改。

發生在之前

例如,對於此類操作,一個先於另一個執行:

同步和監控:

  • 捕獲監視器(鎖定方法、同步啟動)以及在它之後的同一線程上發生的任何事情。
  • 監視器的返回(方法解鎖,同步結束)以及在它之前的同一線程上發生的任何事情。
  • 返回監視器,然後由另一個線程捕獲它。

寫作和閱讀:

  • 寫入任何變量,然後在同一流中讀取它。
  • 在寫入 volatile 變量之前在同一個線程中的所有內容,以及寫入本身。volatile read 和它之後的同一個線程上的所有內容。
  • 寫入 volatile 變量,然後再次讀取它。易失性寫入與內存交互的方式與監視器返回的方式相同,而讀取則類似於捕獲。事實證明,如果一個線程寫入了一個 volatile 變量,而第二個線程找到了它,那麼寫入之前的所有內容都會在讀取之後的所有內容之前執行;看圖片。

對象維護:

  • 靜態初始化和任何對象實例的任何操作。
  • 寫入構造函數中的最終字段以及構造函數之後的所有內容。作為例外,happens-before 關係不會傳遞連接到其他規則,因此可能導致線程間競爭。
  • 使用對象和finalize()的任何工作。

流媒體服務:

  • 啟動線程和線程中的任何代碼。
  • 與線程和線程中的任何代碼相關的變量清零。
  • thread 和join()中的代碼;線程中的代碼和isAlive() == false
  • interrupt()線程並檢測到它已停止。

發生在工作細微差別之前

釋放先行監視器發生在獲取同一監視器之前。值得注意的是是release,而不是exit,即使用wait時不用擔心安全問題。

讓我們看看這些知識將如何幫助我們糾正我們的例子。在這種情況下,一切都非常簡單:只需刪除外部檢查並保持同步不變。現在第二個線程可以保證看到所有的變化,因為它只有在另一個線程釋放它之後才會得到監視器。並且由於他不會在一切都初始化之前釋放它,所以我們將立即看到所有的變化,而不是分開:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

寫入 volatile 變量發生在從同一變量讀取之前。當然,我們所做的更改修復了錯誤,但它會將編寫原始代碼的人放回原處 - 每次都阻塞。volatile關鍵字可以保存。事實上,上述語句意味著當讀取所有聲明為 volatile 的內容時,我們將始終獲得實際值。

另外,前面說了,對於volatile字段,寫總是(包括l​​ong和double)一個原子操作。另一個要點:如果你有一個 volatile 實體,它引用了其他實體(例如,數組、List 或其他一些類),那麼只有對實體本身的引用將始終是“新鮮的”,而不是對它傳入。

那麼,回到我們的雙鎖公羊。使用 volatile,你可以像這樣解決這個問題:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

這裡我們仍然有一個鎖,但前提是 data == null。我們使用 volatile read 過濾掉剩餘的情況。volatile store happens-before volatile read 確保了正確性,並且構造函數中發生的所有操作對於讀取字段值的人都是可見的。