volatile可見性的理解
在多執行緒併發程式設計中synchronized和Volatile都扮演著重要的角色,Volatile是****輕量級的synchronized,它在多處理器開發中保證了共享變數的“可見性”。可見性的意思是當一個執行緒修改一個共享變數時,另外一個執行緒能讀到這個修改的值。它在某些情況下比synchronized的開銷更小。
Volatile的官方定義
Java語言規範第三版中對volatile的定義如下: java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致的更新,執行緒應該確保通過排他鎖單獨獲得這個變數。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個欄位被宣告成volatile,java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的。
為什麼要使用Volatile
Volatile變數修飾符如果使用恰當的話,它比synchronized的使用和執行成本會更低,因為它不會引起執行緒上下文的切換和排程。
記憶體可見性
由於 Java
記憶體模型( JMM
)規定,所有的變數都存放在主記憶體中,而每個執行緒都有著自己的工作記憶體(快取記憶體)。
執行緒在工作時,需要將主記憶體中的資料拷貝到工作記憶體中。這樣對資料的任何操作都是基於工作記憶體(效率提高),並且不能直接操作主記憶體以及其他執行緒工作記憶體中的資料,之後再將更新之後的資料重新整理到主記憶體中。
這裡所提到的主記憶體可以簡單認為是堆記憶體,而工作記憶體則可以認為是棧記憶體
。
如下圖所示:
所以在併發執行時可能會出現執行緒 B 所讀取到的資料是執行緒 A 更新之前的資料。
顯然這肯定是會出問題的,因此 volatile
的作用出現了:
當一個變數被
volatile
修飾時,任何執行緒對它的寫操作都會立即重新整理到主記憶體中,並且會強制讓快取了該變數的執行緒中的資料清空,必須從主記憶體重新讀取最新資料。
volatile
修飾之後並不是讓執行緒直接從主記憶體中獲取資料,依然需要將變數拷貝到工作記憶體中。
本文主要針對 共享變數 可見性理解的演示。
程式碼片段1
public class T01_HelloVolatile { /*volatile*/ boolean running = true; //對比一下有無volatile的情況下,整個程式執行結果的區別 void m() { System.out.println("m 修改【running】值前進行啟動 ,【running】的值為" + running); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } while (running) { } System.out.println("m end!"); } void n() { System.out.println("n 進行【running】的修改,修改前 【running】值為" + running); running = false; System.out.println("n 進行【running】的修改,修改後 【running】值為" + running); } void f() { System.out.println("f 修改【running】值前進行啟動 ,【running】的值為" + running); while (running) { } System.out.println("f end ,【running】的值為" + running); } public static void main(String[] args) throws InterruptedException { T01_HelloVolatile t = new T01_HelloVolatile(); new Thread(t::m, "t1").start(); TimeUnit.SECONDS.sleep(5); new Thread(t::n, "t2").start(); TimeUnit.SECONDS.sleep(1); new Thread(t::f, "t3").start(); } } ``` 此段程式碼演示了 volatile . - 先啟動執行緒 m,m一直持有 running變數。 - 在啟動執行緒n,n執行緒進行running屬性的更改。 - 在啟動執行緒f ,在f執行緒裡面讀取 running變數,驗證是否可以拿到running修改後的變數。 以上程式碼輸出結果: m 修改【running】值前進行啟動 ,【running】的值為true n 進行【running】的修改,修改前 【running】值為true n 進行【running】的修改,修改後 【running】值為false f 修改【running】值前進行啟動 ,【running】的值為false f end ,【running】的值為false 結果顯示 m 執行緒未結束,f執行緒結束 以上說明, 執行緒n修改了running值之後,之後的f執行緒來進行訪問這個值的時候是能訪問到修改的內容的。線上程n修改這個值之前啟動的執行緒m是沒法訪問到執行緒n修改的這個值的內容的。
程式碼片段2
public class T01_HelloVolatile {
/*volatile*/ boolean running = true; //對比一下有無volatile的情況下,整個程式執行結果的區別
void m() {
System.out.println("m 修改【running】值前進行啟動 ,【running】的值為" + running);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (running) {
// 此處千萬不要 使用 System.out.println 此句程式碼會觸發同步機制
}
System.out.println("m end!");
}
void n() {
System.out.println("n 進行【running】的修改,修改前 【running】值為" + running);
running = false;
System.out.println("n 進行【running】的修改,修改後 【running】值為" + running);
}
void f() {
System.out.println("f 修改【running】值前進行啟動 ,【running】的值為" + running);
while (running) {
}
System.out.println("f end ,【running】的值為" + running);
}
public static void main(String[] args) throws InterruptedException {
T01_HelloVolatile t = new T01_HelloVolatile();
new Thread(t::m, "t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(t::n, "t2").start();
TimeUnit.SECONDS.sleep(1);
new Thread(t::f, "t3").start();
}
}
此段程式碼比 程式碼片段1 中 m執行緒多了一段睡眠時間,加上 5 秒的睡眠時間之後,執行結果如下:
m 修改【running】值前進行啟動 ,【running】的值為true
n 進行【running】的修改,修改前 【running】值為true
n 進行【running】的修改,修改後 【running】值為false
f 修改【running】值前進行啟動 ,【running】的值為false
f end ,【running】的值為false
m end!
m 執行緒正常結束。 那麼 片段1 中的總結 就不正確,應該正確理解為 一直持有的這個變數 通過volatile 修飾後,當其他執行緒修改這個變數之後,這個變數在當前執行緒就可見
程式碼片段3
public class T01_HelloVolatile {
volatile boolean running = true; //對比一下有無volatile的情況下,整個程式執行結果的區別
void m() {
System.out.println("m 修改【running】值前進行啟動 ,【running】的值為" + running);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (running) {
// 此處千萬不要 使用 System.out.println 此句程式碼會觸發同步機制
}
System.out.println("m end!");
}
void n() {
System.out.println("n 進行【running】的修改,修改前 【running】值為" + running);
running = false;
System.out.println("n 進行【running】的修改,修改後 【running】值為" + running);
}
void f() {
System.out.println("f 修改【running】值前進行啟動 ,【running】的值為" + running);
while (running) {
}
System.out.println("f end ,【running】的值為" + running);
}
public static void main(String[] args) throws InterruptedException {
T01_HelloVolatile t = new T01_HelloVolatile();
new Thread(t::m, "t1").start();
TimeUnit.SECONDS.sleep(5);
new Thread(t::n, "t2").start();
TimeUnit.SECONDS.sleep(1);
new Thread(t::f, "t3").start();
}
}
此段程式碼和程式碼片段1 做比較因為多了 volatile 關鍵字,所以 m f 執行緒都能正常結束,和程式碼片段1 相比
來說明來 volatile 修飾的變數是可見的。