當前位置:首頁 > IT技術 > 編程語言 > 正文

面試官:你說你精通Java并發(fā),給我講講 volatile
2022-02-14 14:11:41

大家好,我是指北君。

PS:最近又趕上跳槽的高峰期,好多粉絲,都問我要有沒有最新面試題,我連日加班好多天,終于整理好了,公眾號回復?【java極客技術PDF】獲取面試寶典吧。

今天來了解一下面試題:你對 volatile 了解多少。要了解 volatile 關鍵字,就得從 Java 內存模型開始。最后到 volatile 的原理。

一、Java 內存模型 (JMM)

大家都知道 Java 程序可以做到一次編寫然后到處運行。這個功勞要歸功于 Java 虛擬機。Java 虛擬機中定義了一種 Jva 內存模型(JMM),用來屏蔽掉各種硬件和操作系統(tǒng)之間內存訪問差異,讓 Java 程序可以在各個平臺中訪問變量達到相同的效果。

JMM 的主要目標是定義了程序中變量的訪問規(guī)則,就是內存中存放和讀取變量的一些底層的細節(jié)。

JMM 規(guī)則


  1. 變量包含實例字段,靜態(tài)字段,構成數(shù)組對象的元素,不包含局部變量和方法參數(shù)。
  2. 變量都存儲在主內存上。
  3. 每個線程在 CPU 中都有自己的工作內存,工作內存保存了被該線程使用到的變量的主內存副本拷貝。
  4. 線程對變量的所有操作都只能在工作內存,不能直接讀寫主內存的變量。
  5. 不同線程之間無法之間訪問對方工作內存中的變量。

面試官:你說你精通Java并發(fā),給我講講 volatile_java

定義一個靜態(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 種操作來完成,每個操作都是原子性的。


  1. lock (鎖定): 作用于主內存變量,把一個變量標識為一條內存獨占的狀態(tài)。
  2. unlock (解鎖): 作用于主內存變量,把 lock 狀態(tài)的變量釋放出來,釋放出來后才能被其他線程鎖定。
  3. read (讀取): 作用于主內存變量,把一個變量的值從主內存?zhèn)鬏數(shù)焦ぷ鲀却嬷小?/li>
  4. load (載入): 作用于工作內存變量,把 read 操作的變量放入到工作內存副本中。
  5. use (使用): 作用于工作內存變量,把工作內存中的變量的值傳遞給執(zhí)行引擎,每當虛擬機遇到需要這個變量的值的字節(jié)碼指令時都執(zhí)行這個操作。
  6. assgin (賦值): 作用于工作內存變量,把從執(zhí)行引擎收到的值賦值給工作內存變量,每當虛擬機遇到需要賦值變量的值的字節(jié)碼指令時都執(zhí)行這個操作。
  7. store (存儲): 作用于工作內存變量,把工作內存中的一個變量值,傳送到主內存。
  8. write (寫入): 作用于主內存變量,把 store 操作的從工作內存取到的變量寫入主內存變量中。

面試官:你說你精通Java并發(fā),給我講講 volatile_重排序_02

從上圖中可知,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ù)以下策略將內存屏障插入到指令中,禁止重排序:


  1. 在 volatile 寫操作之前插入 StoreStore 屏障。禁止和 StoreStore 屏障之前的普通寫操作不會進行重排序。
  2. 在 volatile 寫操作之后插入 StoreLoad 屏障。禁止和 StoreLoad 屏障之后的 volatile 讀寫重排序。
  3. 在 volatile 讀操作之后插入 LoadLoad 屏障。禁止和 LoadLoad 之后的普通讀和 volatile 讀重排序。
  4. 在 volatile 寫操作之后插入 LoadStore 屏障。禁止和 LoadStore 屏障之后的普通寫操作重排序。

總結

面試被問到 volatile 的時候,可以從 Java 內存模型到原子性、有序性、可見性,最后到 volatile 的原理:內存屏障和 lock 前綴指令。


< END >

告訴大家一個好消息,Java極客技術讀者交流群(摸魚為主),時隔 2 年后再次開放了,感興趣的朋友,可以在公號回復:999

面試官:你說你精通Java并發(fā),給我講講 volatile_重排序_03


本文摘自 :https://blog.51cto.com/u

開通會員,享受整站包年服務立即開通 >