volatile 是 Java 中的關鍵字,是一個變量修飾符,被用來修飾會被不一樣線程訪問和修改的變量。緩存
1. 可見性安全
可見性是指多個線程訪問同一個變量時,其中一個線程修改了該變量的值,其它線程可以當即看到修改的值。多線程
在 Java 內存模型中,全部的變量都存儲在主存中,同時每一個線程都擁有本身的工做線程,用於提升訪問速度。線程會從主存中拷貝變量值到本身的工做內存中,而後在本身的工做線程中操做變量,而不是直接操做主存中的變量,因爲每一個線程在本身的內存中都有一個變量的拷貝,就會形成變量值不一致的問題。併發
以下面的代碼所示:app
測試類:測試
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(); } }
以上測試代碼中,一個線程進行賦值操做,另外一個線程取值,運行該測試代碼能夠發現,很容易阻塞在循環等待中。this
這是由於寫線程寫入一個新值,同時將 hasNewValue 置爲 true,可是隻更新了寫線程本身工做線程的緩存值,沒有更新主存中的值。而讀線程在獲取新值是,其工做線程中的 hasNewValue 爲 false,會陷入到循環等待中,即便寫線程寫了新值,讀線程也沒法獲取。由於讀線程沒有獲取都新值,寫線程的 hasNewValue 沒有被置回 false,因此寫線程也會陷入到循環等待中。所以產生了死鎖。spa
使用 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++ 是一個複合操做,包括三個部分:
volatile 對於這三步操做是沒法保證原子性的,因此會出現上述運行結果。
因此,vloatile 並不能解決全部同步的問題
3. 有序性
在 Java 內存模型中,容許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,可是會影響到多線程併發執行的正確性。
volatile 關鍵字能夠禁止指令從新排序,能夠保證必定的有序性。
volatile 修飾的變量的有序性有兩層含義:
3.1 happen-before
happen-before 關係是用來判斷是否存在數據競爭、線程是否安全的主要依據,也是指令重排序的依據,保證了多線程下的可見性。
volatile 修飾的變量在讀寫時會創建 happen-before 關係。
以下面的測試類:
class VolatileOrder { int i = 0; volatile boolean flag = false; public void write() { i = 1; // 步驟 1 flag = true; // 步驟 2 } public String get() { if (flag) { // 步驟 3 System.out.println("i = " + i); // 步驟 4 } } }
上面的代碼依據 happen-before 原則(關於 happen-before 原則可自行搜索)會創建以下的關係:
因此 步驟 1 對於 步驟 4 是可見的,即變量 i 在多個線程中具備可見性。
這也解釋了 volatile 有序性的第一層含義:全部在 volatile 修飾的變量寫操做以前的寫操做,將會對隨後該 volatile 修飾的變量讀操做以後的語句可見。
利用這個特性能夠優化變量在線程間的可見性,不須要對每一個變量都用 volatile 修飾,只須要用 volatile 修飾一部分變量便可保證其它變量在多線程間也具備可見性。
3.2 禁止 JVM 重排序
對於上述代碼,若是變量 flag 沒有使用 volatile 修飾,那麼步驟 1 和步驟 2 就有可能被 JVM 重排序,就沒法獲得上述的 happen-before 關係,因此 volatile 修飾的變量禁止 JVM 重排序。
以下代碼所示:
class VolatileOrder { int a, b, c; volatile int d; void write() { a = 1; b = 2; c = 3; d = 4; } void read() { int D = d; int A = a; int B = b; int C = c; } }
在 write() 方法中:
a = 1; b = 2; c = 3;
JVM 可能會重排序這三個指令,可是這三個指令必定是排在 d = 4; 這個指令以前。
一樣的,在 read() 方法中:
int A = a; int B = b; int C = c;
JVM 可能會重排序這三個指令,可是這三個指令必定是排在 int D = d; 這個指令以後。