大家好,我是指北君。
PS:最近又趕上跳槽的高峰期,好多粉絲,都問(wèn)我要有沒(méi)有最新面試題,我連日加班好多天,終于整理好了,公眾號(hào)回復(fù)?【java極客技術(shù)PDF】獲取面試寶典吧。
今天來(lái)了解一下面試題:你對(duì) volatile 了解多少。要了解 volatile 關(guān)鍵字,就得從 Java 內(nèi)存模型開始。最后到 volatile 的原理。
一、Java 內(nèi)存模型 (JMM)
大家都知道 Java 程序可以做到一次編寫然后到處運(yùn)行。這個(gè)功勞要?dú)w功于 Java 虛擬機(jī)。Java 虛擬機(jī)中定義了一種 Jva 內(nèi)存模型(JMM),用來(lái)屏蔽掉各種硬件和操作系統(tǒng)之間內(nèi)存訪問(wèn)差異,讓 Java 程序可以在各個(gè)平臺(tái)中訪問(wèn)變量達(dá)到相同的效果。
JMM 的主要目標(biāo)是定義了程序中變量的訪問(wèn)規(guī)則,就是內(nèi)存中存放和讀取變量的一些底層的細(xì)節(jié)。
JMM 規(guī)則
- 變量包含實(shí)例字段,靜態(tài)字段,構(gòu)成數(shù)組對(duì)象的元素,不包含局部變量和方法參數(shù)。
- 變量都存儲(chǔ)在主內(nèi)存上。
- 每個(gè)線程在 CPU 中都有自己的工作內(nèi)存,工作內(nèi)存保存了被該線程使用到的變量的主內(nèi)存副本拷貝。
- 線程對(duì)變量的所有操作都只能在工作內(nèi)存,不能直接讀寫主內(nèi)存的變量。
- 不同線程之間無(wú)法之間訪問(wèn)對(duì)方工作內(nèi)存中的變量。
定義一個(gè)靜態(tài)變量: static int a = 1;
線程 1 工作內(nèi)存 | 指向 | 主內(nèi)存 | 操作 |
-- | -- | a = 1 | -- |
a = 1 | <-- | a = 1 | 線程 1 拷貝主內(nèi)存變量副本 |
a = 3 | -- | a = 1 | 線程 1 修改工作內(nèi)存變量值 |
a = 3 | --> | a = 3 | 線程 1 工作內(nèi)存變量存儲(chǔ)到主內(nèi)存變量 |
上面的一系列內(nèi)存操作,在 JMM 中定義了 8 種操作來(lái)完成。
JMM 交互
主內(nèi)存和工作內(nèi)存之間的交互,JMM 定義了 8 種操作來(lái)完成,每個(gè)操作都是原子性的。
- lock (鎖定): 作用于主內(nèi)存變量,把一個(gè)變量標(biāo)識(shí)為一條內(nèi)存獨(dú)占的狀態(tài)。
- unlock (解鎖): 作用于主內(nèi)存變量,把 lock 狀態(tài)的變量釋放出來(lái),釋放出來(lái)后才能被其他線程鎖定。
- read (讀取): 作用于主內(nèi)存變量,把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中。
- load (載入): 作用于工作內(nèi)存變量,把 read 操作的變量放入到工作內(nèi)存副本中。
- use (使用): 作用于工作內(nèi)存變量,把工作內(nèi)存中的變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到需要這個(gè)變量的值的字節(jié)碼指令時(shí)都執(zhí)行這個(gè)操作。
- assgin (賦值): 作用于工作內(nèi)存變量,把從執(zhí)行引擎收到的值賦值給工作內(nèi)存變量,每當(dāng)虛擬機(jī)遇到需要賦值變量的值的字節(jié)碼指令時(shí)都執(zhí)行這個(gè)操作。
- store (存儲(chǔ)): 作用于工作內(nèi)存變量,把工作內(nèi)存中的一個(gè)變量值,傳送到主內(nèi)存。
- write (寫入): 作用于主內(nèi)存變量,把 store 操作的從工作內(nèi)存取到的變量寫入主內(nèi)存變量中。
從上圖中可知,JMM 交互在一條線程中是不會(huì)出現(xiàn)任何的問(wèn)題。但是當(dāng)有兩條線程的時(shí)候,線程 1 已經(jīng)修改了變量的值,但是并未刷新到主內(nèi)存時(shí),如果此時(shí)線程 2 讀取變量得到的值并不是線程 1 修改過(guò)的數(shù)據(jù)。
當(dāng)引入線程 2 的時(shí)候 定義一個(gè)靜態(tài)變量: static int a = 1;
操作順序 | 線程 1 工作內(nèi)存 | 線程 2 工作內(nèi)存 | 指向 | 主內(nèi)存 | 操作 |
-- | -- | -- | -- | a = 1 | -- |
1 | a = 1 | -- | <-- | a = 1 | 線程 1 拷貝主內(nèi)存變量副本 |
2 | a = 3 | -- | -- | a = 1 | 線程 1 修改工作內(nèi)存變量值 |
3 | a = 3 | -- | --> | a = 1 | 線程 1 工作內(nèi)存變量存儲(chǔ)到主內(nèi)存變量,主內(nèi)存變量還未更新 |
4.1 | a = 3 | a = 1 | <-- | a = 3 | 線程 2 拷貝主內(nèi)存變量副本隨后主內(nèi)存變量更新線程 1 工作內(nèi)存變量 |
4.2 | a = 3 | a = 1 | <-- | a = 3 | 線程 1 工作內(nèi)存變量存儲(chǔ)到主內(nèi)存變量隨后線程 2 獲取主內(nèi)存變量副本 |
下面就可以用 volatile 關(guān)鍵字解決問(wèn)題。
二、volatile
volatile 可以保證變量對(duì)所有線程可見,一條線程修改的值,其他線程對(duì)新值可以立即得知。還可以禁止指令的重排序。
可見性
修改內(nèi)存變量后立刻同步到主內(nèi)存中,其他的線程立刻得知得益于 Java 的先行發(fā)生原則
先行發(fā)生原則中的 volatile 原則:一個(gè) volatile 變量的寫操作先行于后面發(fā)生的這個(gè)變量的讀操作
定義一個(gè)靜態(tài)變量: static int a = 1;
線程 1 工作內(nèi)存 | 線程 2 工作內(nèi)存 | 指向 | 主內(nèi)存 | 操作 |
-- | -- | -- | a = 1 | -- |
a = 1 | -- | <-- | a = 1 | 線程 1 拷貝主內(nèi)存變量副本 |
a = 3 | -- | -- | a = 1 | 線程 1 修改工作內(nèi)存變量值 |
a = 3 | -- | --> | a = 1 | 線程 1 工作內(nèi)存變量存儲(chǔ)到主內(nèi)存變量 |
a = 3 | a = 3 | <-- | a = 3 | volatile 原則: 主內(nèi)存變量保存線程A工作內(nèi)存變量操作在線程 2 工作內(nèi)存讀取主內(nèi)存變量操作之前 |
可見性原理
對(duì) volatile 修飾的變量,在執(zhí)行寫操作的時(shí)候會(huì)多出一條 lock 前綴的指令。JVM 將 lock 前綴指令發(fā)送給 CPU ,CPU 處理寫操作后將最后的值立刻寫回主內(nèi)存,因?yàn)橛?MESI 緩存一致性協(xié)議保證了各個(gè) CPU 的緩存是一致的,所以各個(gè) CPU 緩存都會(huì)對(duì)總線進(jìn)行嗅探,本地緩存中的數(shù)據(jù)是否被別的線程修改了。
如果別的線程修改了共享變量的數(shù)據(jù),那么 CPU 就會(huì)將本地緩存的變量數(shù)據(jù)過(guò)期掉,然后這個(gè) CPU 上執(zhí)行的線程在讀取共享變量的時(shí)候,就會(huì)從主內(nèi)存重新加載最新的數(shù)據(jù)。
原子性
volatile 并不保證變量具有原子性。
public class VolatileTest implements Runnable {
public static volatile int num;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
num++;
}
}
public static void main(String[] args) {
for(int i = 0; i < 100; i++) {
VolatileTest t = new VolatileTest();
Thread t0 = new Thread(t);
t0.start();
}
System.out.println(num);
}
}
這段代碼的結(jié)果有可能不是 100000,有可能小于 100000。因?yàn)?num++ 并不是原子性的。
有序性
volatile 是通過(guò)禁止指令重排序來(lái)保證有序性。為了優(yōu)化程序的執(zhí)行效率 JVM 在編譯 Java 代碼的時(shí)候或者 CPU 在執(zhí)行 JVM 字節(jié)碼的時(shí)候,不影響最終結(jié)果的前提下會(huì)對(duì)指令進(jìn)行重新排序。
編譯器會(huì)根據(jù)以下策略將內(nèi)存屏障插入到指令中,禁止重排序:
- 在 volatile 寫操作之前插入 StoreStore 屏障。禁止和 StoreStore 屏障之前的普通寫操作不會(huì)進(jìn)行重排序。
- 在 volatile 寫操作之后插入 StoreLoad 屏障。禁止和 StoreLoad 屏障之后的 volatile 讀寫重排序。
- 在 volatile 讀操作之后插入 LoadLoad 屏障。禁止和 LoadLoad 之后的普通讀和 volatile 讀重排序。
- 在 volatile 寫操作之后插入 LoadStore 屏障。禁止和 LoadStore 屏障之后的普通寫操作重排序。
總結(jié)
面試被問(wèn)到 volatile 的時(shí)候,可以從 Java 內(nèi)存模型到原子性、有序性、可見性,最后到 volatile 的原理:內(nèi)存屏障和 lock 前綴指令。
< END >
告訴大家一個(gè)好消息,Java極客技術(shù)讀者交流群(摸魚為主),時(shí)隔 2 年后再次開放了,感興趣的朋友,可以在公號(hào)回復(fù):999
本文摘自 :https://blog.51cto.com/u