精通Java中的volatile關鍵字

在一些開源的框架的源碼當中時不時都可以看到volatile這個關鍵字,最近特意學習一下volatile關鍵字的使用方法。

很多資料中是這樣介紹volatile關鍵字的:

volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。

文字不太好理解,通過例子來理解。

1、例子

首先看一個沒有使用volatile關鍵字例子:

package com.swnote.java;

/**
* volatile測試例子
*
* @author lzj
* @date [2019-04-47]
*/
public class VolatileTest {

private boolean flag;

public static void main(String[] args) {
VolatileTest test = new VolatileTest();
test.test();
}

public void test() {
new Thread(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}

flag = true;
}).start();

new Thread(() -> {
while (true) {
if (flag) {
System.out.println("thread flag = " + flag);
}
}
}).start();
}
}

該例子中定義了一個flag共享變量,test方法裡面開啟了兩個線程,第一個線程在等待1秒中後修改共享變量flag的值為true,第二個線程通過循環判斷flag的值,當flag的值為true時,輸出內容。

此時有兩種猜想:

  • 執行後,可以看到輸出內容,即說明第二個線程能夠感知到第一個線程對共享變量flag的修改
  • 執行後,沒有任務內容,即說明第二個線程無法感知到第一個線程對共享變量flag的修改

然後執行結果為:

精通Java中的volatile關鍵字

沒有任務的輸出內容,即證明了此時這樣情況下第二個線程無法感知到第一個線程對共享變量flag的修改的

現在修改一下例子,即為flag變量加上volatile關鍵字,即:

private volatile boolean flag;

然後再運行,此時結果為:

精通Java中的volatile關鍵字

此時就有內容輸出了,說明加上volatile關鍵字後,第二個線程可以感知到第一個線程對共享變量flag的修改的,這就是上面概念中所說的volatile在多處理器開發中保證了共享變量的“可見性”。

2、原理

通過上面的例子證明了volatile在多處理器開發中保證了共享變量的“可見性”,那它是怎麼實現的呢?

這時就得介紹一下Java的內存模型了,首先看如下示意圖:

精通Java中的volatile關鍵字

Java內存模型是如上面所示的:

共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的所有操作都必須在自己的本地內存中進行,而不能直接讀寫主內存中的變量。

根據此理解,上述例子的在沒有加volatile時的情況是這樣的:

第一個線程從主內存中獲取共享變量flag的值,此時值為false,將該值放到自己的本地內存中,然後對變量進行修改,將值改為true,此時也只是將本地內存中flag的值改為了true,此時還沒有將值同步到主內存中,然後第二線程也是將共享變量flag的值放到自己的本地內存中,而此時flag的值還是為false,所以就是一直沒有內容輸出了。

然而加上volatile關鍵字後,第一個線程對flag的修改會強制刷新到主內存中去,同時還會導致其他線程中的本地內存的值會無效,需要重新到主內存獲取,這樣就保證了第一個線程對flag修改後,第二線程能夠感知到。

3、注意點

volatile是輕量級的synchronized,但是它是不能夠代替synchronized的,因為volatile只能保證原子性操作的安全,對於複合操作,volatile是不能保證線程安全的。

例如:

package com.swnote.java;

/**
* 複合操作例子
*
* @author lzj
* @date [2019-04-27]
*/
public class StatisticTest {
private volatile int num = 0;

public static void main(String[] args) {
StatisticTest test = new StatisticTest();
test.statistic();
}

public void statistic() {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
num++;
}).start();
}

System.out.println("num = " + num);
}
}

期望的運行結果是20,可是幾乎每次運行結果都是不一樣的,例如有的結果為:

精通Java中的volatile關鍵字

這是因為num++這個操作不是原子性的,所以即使使用了volatile關鍵字,也是不能保證安全的。


分享到:


相關文章: