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