Java 多線程(八)Java 內(nèi)存模型

什么是內(nèi)存模型

假設(shè)一個線程為變量 aVariable 賦值:

aVariable = 3;

內(nèi)存模型需要解決的問題是:在什么條件下,讀取aVariable的線程將看到這個值為3.

首先我們要了解:

  • 在編譯器中生成的指令順序可以與源代碼中的順序不同;此外編譯器還會把變量保存在寄存器而不是內(nèi)存中
  • 處理器可以采用亂序或并行的方式來執(zhí)行指令
  • 緩存可能會改變將寫入變量提交到主內(nèi)存的次序
  • 保存在處理器本地緩存中的值,對于其他處理器是不可見的

這些因素都會使得一個線程無法看到變量的最新值。

為什么會出現(xiàn)這些奇怪操作?
==性能提升==;
近年來為了提升計算性能采取的方式包括:

  • 提升時鐘頻率
  • 采用流水線的超標量執(zhí)行單元
  • 動態(tài)指令調(diào)度
  • 猜測執(zhí)行
  • 多級緩存
  • 編譯器優(yōu)化
    • 指令重排優(yōu)化執(zhí)行
    • 優(yōu)化全局寄存器分配算法

由于時鐘頻率越來越難以提高,很多廠商轉(zhuǎn)而生產(chǎn)多核處理器,因為能提高的只有硬件并行性。

重排序的解釋

在沒有充分同步的程序中,如果調(diào)度器采用不恰當?shù)姆绞絹斫惶鎴?zhí)行不同的線程操作,那么將導致不正確的結(jié)果。更糟糕的是JMM還使得不同線程看到的操作執(zhí)行順序是不同的,從而導致在缺乏同步的情況下,要推斷操作的執(zhí)行順序?qū)⒆兊酶訌碗s。

Java 語言規(guī)范要求 JVM 在線程中維護一種類似串行的語義:只要程序的最終結(jié)果與在嚴格串行環(huán)境中執(zhí)行的結(jié)果相同,就允許類似指令重排序這種操作的存在。

各種使操作延遲或者看似亂序執(zhí)行的不同原因,都可以歸為重排序

平臺的內(nèi)存模型

在內(nèi)存共享的多處理器體系架構(gòu)中,每個處理器都擁有自己的緩存,并且定期與主內(nèi)存進行協(xié)調(diào)。
在不同的處理器架構(gòu)中分別提供了不同級別的緩存一致性,其中一部分只提供最小的保證,即允許不同的處理器在任一時刻從同一個儲存位置看到不同的值。
操作系統(tǒng)、編譯器以及運行時需要彌合這種在硬件能力與線程安全需求之間的差異

確保每個處理器在任一時刻都知道其他處理器正在進行的工作將需要非常大的開銷。在大多數(shù)的時間里,這種信息是不必要的,因此處理器會適當放寬儲存一致性保證,以換取性能的提升。
此外還定義了一些特殊指令(內(nèi)存柵欄柵欄),當需要共享數(shù)據(jù)時,這些指令就能實現(xiàn)額外的儲存協(xié)調(diào)保證。

幸運的是,Java 程序不需要指定內(nèi)存柵欄的位置,只需要通過正確的使用同步來訪問共享數(shù)據(jù)。

Java 內(nèi)存模型

Java 內(nèi)存模型(JMM)是通過各種操作定義的,包括對變量的讀/寫操作,監(jiān)視器的加鎖/釋放,以及線程的啟動和合并操作。

Happens-Before規(guī)則

JMM為程序中的所有操作定義了一個偏序關(guān)系,稱之為Happens-Before。要想保證執(zhí)行操作 B 的線程能看到 A 的結(jié)果(無論 A 和 B 是否在同一個線程中執(zhí)行),那么 A 和 B 之間必須滿足Happens-Before關(guān)系。如果兩個操作之間缺乏Happens-Before關(guān)系,那么JMM可以對它們?nèi)我獾刂嘏判颉?/p>

  • 程序順序規(guī)則如果程序中操作 A 在操作 B 之前,那么在線程中 A 操作將在 B 操作前執(zhí)行
  • 監(jiān)視器鎖規(guī)則在監(jiān)視器上的解鎖操作必須在同一個監(jiān)視器鎖上的加鎖操作之前執(zhí)行(不應(yīng)該是之后嗎。。。。Monitor??)
  • volatile變量規(guī)則對 volatile 比那里的寫入操作必須在對該變量的讀操作之前執(zhí)行。
  • 線程啟動規(guī)則 在線程上對Thread.start()的調(diào)用必須在該線程中執(zhí)行任何操作之前執(zhí)行。
  • 線程結(jié)束規(guī)則 線程中的任何操作都必須在其他線程檢測到該線程已經(jīng)結(jié)束之前執(zhí)行,或者從 Thread.join 中成功返回,或者在調(diào)用 Thread.isAlive時返回 false
  • 中斷規(guī)則當一個線程在另一個線程上調(diào)用 interrupt 時,必須在被中斷線程檢測到 interrupt 調(diào)用前執(zhí)行(通過拋出 InterruptException,或者調(diào)用 isInterrupted 和 interrupted)
  • 終結(jié)器規(guī)則對象的構(gòu)造函數(shù)必須在啟動該對象的終結(jié)器之前執(zhí)行完成
  • 傳遞性如果操作 A 在操作 B 之前執(zhí)行,并且操作 B 在操作 C 之前執(zhí)行,那么操作 A 必須在操作 C 之前執(zhí)行

我們可以通過下圖給出了兩個線程使用同一個鎖時,在他們之間的Happens-Before關(guān)系(監(jiān)視器鎖規(guī)則):

image

在線程 A 內(nèi)部的所有操作都按照它們在源程序中的先后順序來排序,在線程 B 內(nèi)部的操作也是如此。由于 A 釋放了鎖 M,并且 B 隨后獲取了鎖 M,因此 A 中所有在釋放鎖之前的操作,也就位于 B 請鎖求之后的所有操作之前。

對象發(fā)布

當對象缺少Happens-Before關(guān)系時,就可能出現(xiàn)重排序問題,這就解釋了為什么在沒有充分同步的情況下發(fā)布一個對象會導致另一個線程看到一個只被部分構(gòu)造的對象。另一個方面,Happens-Before排序是在內(nèi)存訪問級別上操作的,它是一種『并發(fā)級的匯編語言』

靜態(tài)內(nèi)部類實現(xiàn)對象發(fā)布

利用 JVM 加載機制保證線程安全:

public class InnerStaticSingleton {
    private static class SingletonHolder {
        private static final InnerStaticSingleton INSTANCE = new InnerStaticSingleton();
    }
    public static InnerStaticSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

==如果需要傳參的話,該如何進行靜態(tài)內(nèi)部類的初始化==

DCL 雙重檢查加鎖

確保實例化對象在有 volatile 的聲明的前提下,可以達到線程安全的效果。

public class DCLInstance {
    private static volatile DCLInstance instance;
    private DCLInstance(){}
    public static DCLInstance getInstance() {
        if (instance == null){
            synchronized (DCLInstance.class){
                if (instance == null){
                    instance = new DCLInstance();
                }
            }
        }
        return instance;
    }
}

這里getInstance進行了兩次判空,第一次是為了不必要的同步,第二次是在singleton等于null的情況下才創(chuàng)建實例。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容