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ī)保證以下原子操作:
- lock(鎖定): 作用于主內(nèi)存變量, 標(biāo)識(shí)為線程獨(dú)有
- unlock(解鎖): 作用于主內(nèi)存變量, 把變量釋放
- read(讀取): 作用于主內(nèi)存變量, 把變量傳輸?shù)焦ぷ鲀?nèi)存
- load(載入): 作用于工作內(nèi)存變量, 把read得到的變量放入到工作內(nèi)存的變量副本
- use(使用): 作用于工作內(nèi)存變量, 把工作副本的變量傳遞給執(zhí)行引擎
- assign(賦值): 作用于工作內(nèi)存變量, 把執(zhí)行引擎的值賦值到工作內(nèi)存
- store(存儲(chǔ)): 作用于工作內(nèi)存, 把工作內(nèi)存的值送到主內(nèi)存中
- write(寫(xiě)入): 作用于主內(nèi)存, 把工作內(nèi)存中得到的值放入主內(nèi)存中

lock, unlock, read, load, store, write, assign 和 use 規(guī)則如下:
- 不允許 read 和 load, store 和 write 操作之一單獨(dú)出現(xiàn), 必須同時(shí)出現(xiàn)
- 工作內(nèi)存中的值改變(assign)了必須同步到主內(nèi)存中
- 工作內(nèi)存中的值沒(méi)有改變(assign)不允許同步回主內(nèi)存中
- 一個(gè)新的變量只能在主內(nèi)存中創(chuàng)建, 不能使用未初始化(load 或 assign)的變量, 對(duì)一個(gè)變量執(zhí)行 use,store 之前, 必須先執(zhí)行過(guò)了 assign和load
- 同一個(gè)變量, 同一時(shí)刻, 只能一個(gè)線程對(duì)其 lock 操作, lock 操作可以被同一個(gè)線程執(zhí)行多次, 只有執(zhí)行相同次數(shù)的 unlock 變量才能被解鎖
- 對(duì)一個(gè)變量執(zhí)行 lock 操作, 會(huì)清空工作內(nèi)存中此變量的值, 其他執(zhí)行引擎需要重新 load 或 assign 初始化這個(gè)變量
- 一個(gè)變量沒(méi)有被 lock 鎖定, 不允許 unlock, 不允許 unlock 其他線程鎖住的變量
- 對(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:
- 運(yùn)算結(jié)果不依賴變量當(dāng)前的值, 或者或者能確保只有一個(gè)單一的線程修改變量的值
- 變量不需要與其他的狀態(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ú)序的
- 內(nèi)存模型提供了 lock, unlock 來(lái)滿足用戶更大范圍的原子操作需求, 使用字節(jié)碼 monitorenter, monitorexit 完成操作, Java代碼中用 synchronized 實(shí)現(xiàn)
- 同步塊(synchronized 和 final)的可見(jiàn)性, 在執(zhí)行 unlock 之前, 必須把此變量返回到主內(nèi)存中 (把鎖的對(duì)象返回到主內(nèi)存)
- 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ú)需任何同步就能保障成立.
- 程序次序規(guī)則: 在一個(gè)線程內(nèi)在前面的代碼先行發(fā)生于后面的代碼
- 管程鎖定規(guī)則: 一個(gè) unlock 先行發(fā)生于同一個(gè)鎖的 lock
- volatile 規(guī)則: 一個(gè) volatile 的寫(xiě)操作先行發(fā)生于這個(gè)變量的讀操作, 這里指時(shí)間順序
- 線程啟動(dòng)規(guī)則: Thread 對(duì)象的 start() 先行發(fā)生于此線程的每一個(gè)操作
- 線程終止規(guī)則: 線程中所有操作都先行于此線程的終止檢測(cè)(檢查到線程已經(jīng)終止, 肯定操作都已經(jīng)結(jié)束)
- 線程中斷規(guī)則: 對(duì)線程 interrupt() 方法的調(diào)用先行于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
- 傳遞性: A 先行與 B, B 先行于 C, 則 A 先行于 C
- 兩個(gè)線程操作一個(gè)普通共享變量, 不符合上面的任何一個(gè)規(guī)則, 因此是不安全的
- 先行發(fā)生不等于時(shí)間上一定先發(fā)生, 同一個(gè)線程中的, 可能指令重排序
參考《深入理解 Java 虛擬機(jī)》
多線程編程 深入理解DCL的安全性
從DCL的對(duì)象安全發(fā)布談起