天天看點

Java記憶體模型

Java 記憶體模型

Java 記憶體模型是通過各種操作來定義的,包括對變量的讀/寫操作,螢幕的加鎖、解鎖操作,以及線程的啟動和合并操作。 <code>JMM</code> 為程式中所有的操作定義了一個偏序關系,稱之為 <code>Happens-Before</code>。如果想要保證執行 B 操作的線程看到操作 A 的結果(無論 A 和 B 是否在同一個線程中執行),那麼 A 和 B 之間的操作必須滿足 <code>Happens-Before</code> 的關系。如果兩個操作之間缺少 <code>Happens-Before</code> 關系,那麼 <code>JVM</code> 就可以對它們進行任意的重排序。

程式順序規則:如果程式中操作 A 在操作 B 之前,那麼線上程中操作 A 将在 操作 B 之前執行

螢幕鎖規則:在螢幕鎖上的解鎖操作必須在同一個螢幕鎖上的加鎖操作之前執行(顯示鎖和内置鎖在加鎖和解鎖等操作上有相同的記憶體語義)

volatile 變量規則:對 volatile 變量的寫入操作必須在對該變量的讀操作之前執行(原子變量與 volatile 變量在讀操作和寫操作上有着相同的語義)

線程啟動規則:線上程上對 <code>Thread.start()</code> 的調用必須在該線程中執行任何操作之前執行

線程結束規則:線程中的任何操作都必須在其他線程檢測到該線程已經結束之前執行,或者從 <code>Thread.join()</code> 中成功傳回,或者在調用 <code>Thread.isAlive()</code> 中傳回 <code>false</code>

中斷規則:當一個線程在另一個線程上調用 <code>interrupt</code> 時,必須在被中斷線程檢測到 <code>interrupt</code> 之前執行(或者抛出 <code>InterruptException</code>,或者調用 <code>isInterrupted</code> 和 <code>interrupted</code>)

終結器規則:對象的構造函數必須在啟動該對象的終結器之前執行

傳遞性:如果操作 A 在操作 B 之前執行,并且 B操作在 C操作之前執行,那麼操作 A 必須在 操作 C 之前執行

線程 A 釋放了一個鎖,實質上是線程 A 向接下來将要擷取這個鎖的某個線程發出了(線程 A 對共享變量所做修改)的消息

線程 B 擷取了一個鎖,實質上是線程 B 接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改)的消息

線程 A 釋放鎖,随後線程 B 獲得了這個鎖,這個過程實質上是線程 A 通過主存向線程 B 發送了消息

線程 A 寫一個 <code>volatile</code> 變量,實質上是線程 A 向接下來将要讀這個 <code>volatile</code> 變量的某個線程發送了對共享變量所做修改的消息

線程 B 讀取一個 <code>volatile</code> 變量,實質上是線程 B 接收了之前某個線程發出的在讀這個 <code>volatile</code> 變量之前所做修改的消息

線程 A 寫一個 <code>volatile</code> 變量,随後線程 B 讀取了這個變量,這個過程實質上是線程 A 通過主存向線程 B 發送了修改這個共享變量的消息

記憶體屏障

為了實作 <code>volatile</code> 的記憶體語義,編譯器會在生成位元組碼時,在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序

記憶體屏障是一種 barrier 指令類型,它導緻 CPU 或編譯器對 barrier 指令前後發出的記憶體操作執行順序限制。也就是說,在 barrier 之前的記憶體操作保證在 barrier 之後的操作之前執行

記憶體屏障主要分為以下四種:

<code>LoadLoad</code>記憶體屏障:對于這樣的語句 <code>load1;LoadLoad;load2</code>,在 <code>load2</code> 及後續讀取操作要讀取的資料被通路之前,保證 <code>load1</code> 要讀取的資料被讀取完畢

<code>StoreStore</code>記憶體屏障:對于這樣的語句 <code>store1;StoreStore;store2</code>,在 <code>store2</code> 及後續的寫入操作執行之前,保證 <code>store1</code> 中的寫入操作對處理器可見

<code>LoadStore</code>記憶體屏障:對于這樣的語句 <code>load1;LoadStore;store1</code>,在 <code>store1</code> 及後續寫入操作被刷出之前,保證 <code>load1</code> 的讀取操作要全部完成

<code>StoreLoad</code>記憶體屏障:對于這樣的語句 <code>store1;StoreLoad;load1</code>,在<code>load1</code> 及後續的所有讀取操作執行之前,保證 <code>store1</code> 中的資料寫入對于所有處理器可見。這個記憶體屏障是所有記憶體屏障中開銷最大的,這個屏障是一個萬能屏障,兼具其他三種記憶體屏障的功能

<code>Java</code> 中 <code>volatile</code> 的實作

對每個<code>volatile</code> 寫操作之前插入一個 <code>StoreStore</code> 記憶體屏障

對每個 <code>volatile</code> 寫操作之後插入一個<code>StoreLoad</code> 記憶體屏障

對每個 <code>volatile</code>讀操作之前插入一個 <code>LoadLoad</code> 記憶體屏障

對每個 <code>volatile</code> 讀操作之後插入一個 <code>LoadStore</code> 記憶體屏障

在構造函數内對一個 <code>final</code> 域的寫入,與随後把這個被構造對象的引用指派給一個引用變量,這兩個操作之間不能重排序

初次讀一個包含 <code>final</code> 域的對象的引用,與随後初次讀這個 <code>final</code> 域,這兩個操作之間不能重排序

寫 <code>final</code> 域的重排序規則禁止把 <code>final</code> 域的寫重排序到構造函數之外,這個規則的實作包含下面兩個方面:

<code>JMM</code> 禁止編譯器把 <code>final</code> 域的寫重排序到構造函數之外

編譯器會在 <code>final</code> 域的寫之後,構造函數的 <code>return</code> 之前,插入一個 <code>StoreStore</code> 記憶體屏障。這個屏障禁止處理器把 <code>final</code> 域的寫重排序到構造函數之外。寫 <code>final</code> 域的重排序規則可以確定:在對象引用為任意線程可見之前,對象的 <code>final</code> 域已經被正确初始化過了,而普通域則不具備這個保障

在一個線程中,初次讀對象引用和初次讀該對象包含的 <code>final</code> 域,<code>JMM</code> 禁止處理器重排序這兩個操作(注意,僅僅隻是針對處理器)

編譯器會在讀 <code>final</code> 域操作前插入一個 <code>LoadLoad</code> 記憶體屏障

初次讀對象引用與初次讀該對象包含的 <code>final</code> 域,這兩個操作之間存在間接依賴關系。由于編譯器遵守間接依賴關系,是以編譯器也不會重排序這兩個操作

大多數處理器也會遵守間接依賴也不會重排序這兩個操作,但是少數處理器允許存在間接依賴關系的操作做重排序,這個規則就是針對這些處理器的。

讀 <code>final</code> 域的重排序規則可以確定:在讀一個 <code>final</code> 域之前,一定會先讀包含這個 <code>final</code> 域的引用

對于引用類型,寫 <code>final</code> 域的重排序規則對編譯器和處理器增加了如下限制:在構造函數内對一個 <code>final</code> 引用的對象的成員域的寫入,與随後在構造函數外把這個被構造對象的引用指派給一個引用變量,這兩個操作之間不能重排序。這一規則確定了其它線程能夠讀到被正确初始化的 <code>final</code> 引用對象的成員域

靜态工廠方法實作單例模式

枚舉類型實作單例模式

延遲初始化類

<code>DCL</code>(雙重檢查鎖)

​ 實際上,一般來講,正常地使用餓漢式地方式來實作單例是最好的解決方案。但是如果确實需要使用延遲化的加載方式,如果需要使用到靜态變量,那麼使用延遲化初始化類的方式實作是最好的;如果不得不使用一個對象的字段來表示單例,那麼就使用 <code>DCL</code> 的方式。

繼續閱讀