今天來了解一下面試題:你對 volatile 了解多少。要了解 volatile 關鍵字,就得從 Java 內(nèi)存模型開始。最后到 volatile 的原理。
一、Java 內(nèi)存模型 (JMM)
大家都知道 Java 程序可以做到一次編寫然后到處運行。這個功勞要歸功于 Java 虛擬機。Java 虛擬機中定義了一種 Jva 內(nèi)存模型(JMM),用來屏蔽掉各種硬件和操作系統(tǒng)之間內(nèi)存訪問差異,讓 Java 程序可以在各個平臺中訪問變量達到相同的效果。
JMM 的主要目標是定義了程序中變量的訪問規(guī)則,就是內(nèi)存中存放和讀取變量的一些底層的細節(jié)。
JMM 規(guī)則
- 變量包含實例字段,靜態(tài)字段,構(gòu)成數(shù)組對象的元素,不包含局部變量和方法參數(shù)。
- 變量都存儲在主內(nèi)存上。
- 每個線程在 CPU 中都有自己的 工作內(nèi)存 ,工作內(nèi)存保存了被該線程使用到的變量的主內(nèi)存副本拷貝。
- 線程對變量的所有操作都只能在工作內(nèi)存,不能直接讀寫主內(nèi)存的變量。
- 不同線程之間無法之間訪問對方工作內(nèi)存中的變量。
定義一個靜態(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)存變量存儲到主內(nèi)存變量 |
上面的一系列內(nèi)存操作,在 JMM 中定義了 8 種操作來完成。
JMM 交互
主內(nèi)存和工作內(nèi)存之間的交互,JMM 定義了 8 種操作來完成,每個操作都是原子性的。
- lock (鎖定): 作用于主內(nèi)存變量,把一個變量標識為一條內(nèi)存獨占的狀態(tài)。
- unlock (解鎖): 作用于主內(nèi)存變量,把 lock 狀態(tài)的變量釋放出來,釋放出來后才能被其他線程鎖定。
- read (讀取): 作用于主內(nèi)存變量,把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中。
- load (載入): 作用于工作內(nèi)存變量,把 read 操作的變量放入到工作內(nèi)存副本中。
- use (使用): 作用于工作內(nèi)存變量,把工作內(nèi)存中的變量的值傳遞給執(zhí)行引擎,每當虛擬機遇到需要這個變量的值的字節(jié)碼指令時都執(zhí)行這個操作。
- assgin (賦值): 作用于工作內(nèi)存變量,把從執(zhí)行引擎收到的值賦值給工作內(nèi)存變量,每當虛擬機遇到需要賦值變量的值的字節(jié)碼指令時都執(zhí)行這個操作。
- store (存儲): 作用于工作內(nèi)存變量,把工作內(nèi)存中的一個變量值,傳送到主內(nèi)存。
- write (寫入): 作用于主內(nèi)存變量,把 store 操作的從工作內(nèi)存取到的變量寫入主內(nèi)存變量中。
從上圖中可知,JMM 交互在一條線程中是不會出現(xiàn)任何的問題。但是當有兩條線程的時候,線程 1 已經(jīng)修改了變量的值,但是并未刷新到主內(nèi)存時,如果此時線程 2 讀取變量得到的值并不是線程 1 修改過的數(shù)據(jù)。
當引入線程 2 的時候 定義一個靜態(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)存變量存儲到主內(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)存變量存儲到主內(nèi)存變量隨后線程 2 獲取主內(nèi)存變量副本 |
下面就可以用 volatile 關鍵字解決問題。
二、volatile
volatile 可以保證變量對所有線程可見,一條線程修改的值,其他線程對新值可以立即得知。還可以禁止指令的重排序。
可見性
修改內(nèi)存變量后立刻同步到主內(nèi)存中,其他的線程立刻得知得益于 Java 的先行發(fā)生原則
先行發(fā)生原則中的 volatile 原則:一個 volatile 變量的寫操作先行于后面發(fā)生的這個變量的讀操作
定義一個靜態(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)存變量存儲到主內(nèi)存變量 |
a = 3 | a = 3 | <-- | a = 3 | volatile 原則: 主內(nèi)存變量保存線程A工作內(nèi)存變量操作在線程 2 工作內(nèi)存讀取主內(nèi)存變量操作之前 |
可見性原理
對 volatile 修飾的變量,在執(zhí)行寫操作的時候會多出一條 lock 前綴的指令。JVM 將 lock 前綴指令發(fā)送給 CPU ,CPU 處理寫操作后將最后的值立刻寫回主內(nèi)存,因為有 MESI 緩存一致性協(xié)議保證了各個 CPU 的緩存是一致的,所以各個 CPU 緩存都會對總線進行嗅探,本地緩存中的數(shù)據(jù)是否被別的線程修改了。
如果別的線程修改了共享變量的數(shù)據(jù),那么 CPU 就會將本地緩存的變量數(shù)據(jù)過期掉,然后這個 CPU 上執(zhí)行的線程在讀取共享變量的時候,就會從主內(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。因為 num++ 并不是原子性的。
有序性
volatile 是通過禁止指令重排序來保證有序性。為了優(yōu)化程序的執(zhí)行效率 JVM 在編譯 Java 代碼的時候或者 CPU 在執(zhí)行 JVM 字節(jié)碼的時候,不影響最終結(jié)果的前提下會對指令進行重新排序。
編譯器會根據(jù)以下策略將內(nèi)存屏障插入到指令中,禁止重排序:
- 在 volatile 寫操作之前插入 StoreStore 屏障。禁止和 StoreStore 屏障之前的普通寫操作不會進行重排序。
- 在 volatile 寫操作之后插入 StoreLoad 屏障。禁止和 StoreLoad 屏障之后的 volatile 讀寫重排序。
- 在 volatile 讀操作之后插入 LoadLoad 屏障。禁止和 LoadLoad 之后的普通讀和 volatile 讀重排序。
- 在 volatile 寫操作之后插入 LoadStore 屏障。禁止和 LoadStore 屏障之后的普通寫操作重排序。
-
內(nèi)存
+關注
關注
8文章
3025瀏覽量
74054 -
JAVA
+關注
關注
19文章
2967瀏覽量
104758 -
模型
+關注
關注
1文章
3243瀏覽量
48842 -
volatile
+關注
關注
0文章
45瀏覽量
13031
發(fā)布評論請先 登錄
相關推薦
評論