天天看點

Java 之 volatile 詳解

一、概念

volatile 是 Java 中的關鍵字,是一個變量修飾符,被用來修飾會被不同線程通路和修改的變量。

二、volatile 作用

1. 可見性

可見性是指多個線程通路同一個變量時,其中一個線程修改了該變量的值,其它線程能夠立即看到修改的值。

在 Java 記憶體模型中,所有的變量都存儲在主存中,同時每個線程都擁有自己的工作線程,用于提高通路速度。線程會從主存中拷貝變量值到自己的工作記憶體中,然後在自己的工作線程中操作變量,而不是直接操作主存中的變量,由于每個線程在自己的記憶體中都有一個變量的拷貝,就會造成變量值不一緻的問題。

如下面的代碼所示:

測試類:

class VolatileTestObj { private String value = null; private boolean hasNewValue = false; public void put(String value) { while (hasNewValue) { // 等待,防止重複指派 } this.value = value; hasNewValue = true; } public String get() { while (!hasNewValue) { // 等待,防止擷取到舊值 } String value = this.value; hasNewValue = false; return value; } }複制代碼

測試代碼:

public class VolatileTest { public static void main(String... args) { VolatileTestObj obj = new VolatileTestObj(); new Thread(() -> { while (true) { obj.put("time:" + System.currentTimeMillis()); } }).start(); new Thread(() -> { while (true) { System.out.println(obj.get()); } }).start(); } }複制代碼

以上測試代碼中,一個線程進行指派操作,另一個線程取值,運作該測試代碼可以發現,很容易阻塞在循環等待中。

這是因為寫線程寫入一個新值,同時将 hasNewValue 置為 true,但是隻更新了寫線程自己工作線程的緩存值,沒有更新主存中的值。而讀線程在擷取新值是,其工作線程中的 hasNewValue 為 false,會陷入到循環等待中,即使寫線程寫了新值,讀線程也無法擷取。因為讀線程沒有擷取都新值,寫線程的 hasNewValue 沒有被置回 false,是以寫線程也會陷入到循環等待中。是以産生了死鎖。

使用 volatile 關鍵字可以解決這個問題,使用 volatile 修飾的變量確定了線程不會将該變量拷貝到自己的工作線程中,所有線程對該變量的操作都是在主存中進行的,是以 volatile 修飾的變量對所有線程可見。

使用 volatile 修飾 hasNewValue,這樣在寫線程和讀線程中都是在主存中操作 hasNewValue 的值,就不會産生死鎖。

2. 原子性

volatile 隻保證單次讀/寫操作的原子性,對于多步操作,volatile 不能保證原子性,如下代碼所示:

測試類:

class VolatileCounter { private volatile int count = 0; public void inc() { count++; } public void dec() { count--; } public int get() { return count; } }複制代碼

測試代碼:

public class VolatileTest { public static void main(String... args) { while (true) { VolatileCounter counter = new VolatileCounter(); Thread thread1 = new Thread(() -> { for (int i = 0; i < 50; i++) { counter.inc(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 50; i++) { counter.dec(); } }); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("counter = " + counter.get()); } } }複制代碼

運作結果:

... counter = 0 counter = 0 counter = 0 counter = 0 counter = -21 counter = 0 counter = 0 counter = 0 counter = 0 ...複制代碼

從運作結果可以看出,絕大部分情況下輸出結果為 counter = 0,但也有部分其它結果。由此可知,對于 count++; 和 count--; 這兩個操作并不具有原子性。

這是因為 count++ 是一個複合操作,包括三個部分:

  1. 讀取 count 的值;
  2. 對 count 加 1;
  3. 将 count 的值寫回記憶體;
  1. 所有在 volatile 修飾的變量寫操作之前的寫操作,将會對随後該 volatile 修飾的變量讀操作之後的語句可見。
  2. 禁止 JVM 重排序:volatile 修飾的變量的讀寫指令不能和其前後的任何指令重排序,其前後的指令可能會被重排序。
  • 根據 happen-before 單線程順序原則會有:步驟 1 happen-before 步驟 2、步驟 3 happen-before 步驟 4;
  • 根據 happen-before 的 volatile 原則會有:步驟 2 happen-before 步驟 3;
  • 根據 happen-before 的傳遞性原則會有:步驟 1 happen-before 步驟 4;