Java 內(nèi)存模型

Java 內(nèi)存模型(Java Memory Model, JMM) 是 Java 為了屏蔽不同硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異, 讓 Java 程序在各種平臺(tái)下都有一致的內(nèi)存訪問(wèn)效果.

Java 內(nèi)存模型的主要目標(biāo)是定義程序中共享變量的訪問(wèn)規(guī)則

由于處理器的速度跟主內(nèi)存的速度不是一個(gè)數(shù)量級(jí)的, 因此處理器讀取內(nèi)存中的數(shù)據(jù)時(shí), 會(huì)讀取一段數(shù)據(jù)并緩存, 處理器不會(huì)直接跟主內(nèi)存交互而是直接使用并修改緩存上的數(shù)據(jù).

當(dāng)同一塊主內(nèi)存的數(shù)據(jù)在多個(gè)線程 (多個(gè)處理器) 中有多份拷貝時(shí), 其中一個(gè)線程對(duì)其緩存的變量修改之后, 不會(huì)馬上寫(xiě)會(huì)到主內(nèi)存, 即使寫(xiě)回了主內(nèi)存, 其他的線程不知道這個(gè)值已經(jīng)改變了, 而是繼續(xù)使用緩存的數(shù)據(jù), 因此會(huì)有數(shù)據(jù)不一致的問(wèn)題(線程不安全).

虛擬機(jī)保證以下原子操作:

  1. lock(鎖定): 作用于主內(nèi)存變量, 標(biāo)識(shí)為線程獨(dú)有
  2. unlock(解鎖): 作用于主內(nèi)存變量, 把變量釋放
  3. read(讀取): 作用于主內(nèi)存變量, 把變量傳輸?shù)焦ぷ鲀?nèi)存
  4. load(載入): 作用于工作內(nèi)存變量, 把read得到的變量放入到工作內(nèi)存的變量副本
  5. use(使用): 作用于工作內(nèi)存變量, 把工作副本的變量傳遞給執(zhí)行引擎
  6. assign(賦值): 作用于工作內(nèi)存變量, 把執(zhí)行引擎的值賦值到工作內(nèi)存
  7. store(存儲(chǔ)): 作用于工作內(nèi)存, 把工作內(nèi)存的值送到主內(nèi)存中
  8. write(寫(xiě)入): 作用于主內(nèi)存, 把工作內(nèi)存中得到的值放入主內(nèi)存中
Java 內(nèi)存模型

lock, unlock, read, load, store, write, assign 和 use 規(guī)則如下:

  1. 不允許 read 和 load, store 和 write 操作之一單獨(dú)出現(xiàn), 必須同時(shí)出現(xiàn)
  2. 工作內(nèi)存中的值改變(assign)了必須同步到主內(nèi)存中
  3. 工作內(nèi)存中的值沒(méi)有改變(assign)不允許同步回主內(nèi)存中
  4. 一個(gè)新的變量只能在主內(nèi)存中創(chuàng)建, 不能使用未初始化(load 或 assign)的變量, 對(duì)一個(gè)變量執(zhí)行 use,store 之前, 必須先執(zhí)行過(guò)了 assign和load
  5. 同一個(gè)變量, 同一時(shí)刻, 只能一個(gè)線程對(duì)其 lock 操作, lock 操作可以被同一個(gè)線程執(zhí)行多次, 只有執(zhí)行相同次數(shù)的 unlock 變量才能被解鎖
  6. 對(duì)一個(gè)變量執(zhí)行 lock 操作, 會(huì)清空工作內(nèi)存中此變量的值, 其他執(zhí)行引擎需要重新 load 或 assign 初始化這個(gè)變量
  7. 一個(gè)變量沒(méi)有被 lock 鎖定, 不允許 unlock, 不允許 unlock 其他線程鎖住的變量
  8. 對(duì)變量執(zhí)行 unlock 之前, 必須先把此變量同步回主內(nèi)存中(store, write)

volatile 特殊規(guī)則

volatile 保證變量的可見(jiàn)性

volatile 修飾的變量, 每次這個(gè)值在一個(gè)線程中被修改時(shí), 會(huì)立即同步到主內(nèi)存中, 并且使其他換處理器緩存了這個(gè)變量的緩存全部失效, 使用這個(gè)變量時(shí), 需要重新從主內(nèi)存中獲取, 因此能保證 volatile 修飾的變量在所有線程中是一致的

但是 volatile 的運(yùn)算不一定是線程安全的, 因?yàn)檫\(yùn)算不一定是線程安全的.

下面的例子, 開(kāi)啟了20個(gè)線程, 每個(gè)線程執(zhí)行 10000 次 race++, 實(shí)際運(yùn)行結(jié)果會(huì)小于 200000.

volatile static int race = 0;
for (int i=0; i<20; i++) {
    threads[i] = new Thread(() -> {
            for (int i=0; i<10000; i++) race ++;
        });
    threads[i].start();
}

為了方便理解, 這里舉一個(gè)例子特殊說(shuō)明為啥最終值會(huì)比200000小. 假設(shè)有兩個(gè)線程都緩存了變量 race, 值設(shè)為63. 都執(zhí)行了 race++ 操作, 并且都得到了結(jié)果(結(jié)果都是64), 當(dāng)其中一個(gè)線程把值(64)寫(xiě)入到主內(nèi)存后, 另一個(gè)線程不會(huì)使用新的race值再次計(jì)算一遍, 而是直接把64寫(xiě)回了主內(nèi)存中, 因此經(jīng)過(guò)兩次 race++, 值只增加了1.

只有滿足下面兩個(gè)規(guī)則才能使用 volatile:

  1. 運(yùn)算結(jié)果不依賴變量當(dāng)前的值, 或者或者能確保只有一個(gè)單一的線程修改變量的值
  2. 變量不需要與其他的狀態(tài)共同參與不變約束

volatile 禁止指令的重排優(yōu)化

對(duì)于普通的變量, 虛擬機(jī)只保證單線程中運(yùn)行出來(lái)的結(jié)果是正確的, 不能保證指令的執(zhí)行順序(在單線程中, 感知不到指令發(fā)生重排)

給 volatile 變量賦值之后, 會(huì)執(zhí)行 lock addl $0x0, 把數(shù)據(jù)寫(xiě)入到主內(nèi)存, 并讓其他線程的緩存無(wú)效. 對(duì)與普通變量, 為了提高效率 CPU 會(huì)將指令亂序, 只保正最終結(jié)果一樣.

大多數(shù)情況下 volatile 比鎖的開(kāi)銷低, volatile 變量的讀取跟普通變量一樣, 寫(xiě)入的時(shí)候要用到內(nèi)存屏障(后面的指令無(wú)法跑到屏障之前的位置), 需要更多的時(shí)間

對(duì)于 long 和 double 變量的特殊規(guī)則

Java規(guī)范允許沒(méi)有聲明為 volatile 的 long, double 型變量分兩次操作(不是原子操作), 但是也強(qiáng)烈建議不這么做, 基本上的機(jī)器都當(dāng)成原子操作

原子性, 可見(jiàn)性, 有序性

  • 原子性: 由 Java 內(nèi)存模型包裝原子性 read, load, assign, use, store, write, lock, unlock 都是原子的
  • 可見(jiàn)性: 變量的值被修改之后, 都會(huì)同步到主內(nèi)存, 使用 volatile 保證修改之后馬上同步會(huì)主內(nèi)存
  • 有序性: 在一個(gè)線程中, 所有的操作都是有序的, 在另一個(gè)線程中觀察, 所有的操作都是無(wú)序的
  1. 內(nèi)存模型提供了 lock, unlock 來(lái)滿足用戶更大范圍的原子操作需求, 使用字節(jié)碼 monitorenter, monitorexit 完成操作, Java代碼中用 synchronized 實(shí)現(xiàn)
  2. 同步塊(synchronized 和 final)的可見(jiàn)性, 在執(zhí)行 unlock 之前, 必須把此變量返回到主內(nèi)存中 (把鎖的對(duì)象返回到主內(nèi)存)
  3. final 修飾的字段, 一旦對(duì)象把 this 傳遞出去, 其他線程就能看到 final 的值, 如果沒(méi)有使用 final 修飾, 可能這個(gè)對(duì)象傳遞出去之后, 未初始化完, 其他線程調(diào)用可能出問(wèn)題. (沒(méi)有使用 final 修飾的字段, 可能對(duì)象創(chuàng)建之后, 對(duì)象的字段未初始化)

先行發(fā)生原則(happens-before)

內(nèi)存模型中緊靠 volatile 和 synchronized 會(huì)使操作變得繁瑣, 因此有了先行發(fā)生的原則, 先行發(fā)生規(guī)則無(wú)需任何同步就能保障成立.

  1. 程序次序規(guī)則: 在一個(gè)線程內(nèi)在前面的代碼先行發(fā)生于后面的代碼
  2. 管程鎖定規(guī)則: 一個(gè) unlock 先行發(fā)生于同一個(gè)鎖的 lock
  3. volatile 規(guī)則: 一個(gè) volatile 的寫(xiě)操作先行發(fā)生于這個(gè)變量的讀操作, 這里指時(shí)間順序
  4. 線程啟動(dòng)規(guī)則: Thread 對(duì)象的 start() 先行發(fā)生于此線程的每一個(gè)操作
  5. 線程終止規(guī)則: 線程中所有操作都先行于此線程的終止檢測(cè)(檢查到線程已經(jīng)終止, 肯定操作都已經(jīng)結(jié)束)
  6. 線程中斷規(guī)則: 對(duì)線程 interrupt() 方法的調(diào)用先行于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
  7. 傳遞性: A 先行與 B, B 先行于 C, 則 A 先行于 C
  1. 兩個(gè)線程操作一個(gè)普通共享變量, 不符合上面的任何一個(gè)規(guī)則, 因此是不安全的
  2. 先行發(fā)生不等于時(shí)間上一定先發(fā)生, 同一個(gè)線程中的, 可能指令重排序

參考《深入理解 Java 虛擬機(jī)》
多線程編程 深入理解DCL的安全性
從DCL的對(duì)象安全發(fā)布談起

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

相關(guān)閱讀更多精彩內(nèi)容

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