volatile 的語義之一,意思是一個線程修改了共享變量時,另一個線程能夠讀到這個修改后的值。
每個線程都擁有獨有的本地內存,而 JMM 控制著主內存和本地內存的數據交換。
public class VolatileExample01 { public static void main(String[] args) throws Exception { MyThread thread = new MyThread(); thread.start(); try { Thread.sleep(3000); } finally { thread.setFlag(true); } }}class MyThread extends Thread { PRivate boolean flag = false; @Override public void run() { while (!flag) { //System.out.println("Running..."); } } public void setFlag(boolean flag) { this.flag = flag; }}上述代碼運行起來有可能形成死循環。
過程分析: 上述代碼中主線程將 flag 讀取到本地內存進行修改,然后刷新到主內存;然而線程 thread 一直在讀取其本地內存中的 flag 值,無法看到主線程對 flag 變量做的修改,因此造成死循環。
解決辦法: 用 volatile 修飾共享變量
volatile private boolean flag = false結論: volatile 修飾共享變量能夠保證其的可見性。
原理:【附錄】volatile 的內存語義。
此為 volatile 的語義之一。 查看:重排序 原理:【附錄】volatile 的內存語義。
如上代碼運行三次,輸出的結果分別為:
9998、9941、9895
都沒有達到理想的結果 10000;
原因分析: 此處 count++ 不具有原子性。
結論: volatile 不具有原子性,不能保證變量同步; 要保證變量同步還得用 synchronized 關鍵字。
修改上述代碼,去掉 volatile 關鍵字,使用 synchronized 將 add() 同步,如下:
public class SynchronizedExample01 { public static void main(String[] args) throws Exception { Mythread02[] threadArray = new Mythread02[100]; for (int i = 0; i < 100; i++) { threadArray[i] = new Mythread02(); } for (int i = 0; i < 100; i++) { threadArray[i].start(); } Thread.sleep(10000); System.out.println(Mythread02.getCount()); }}class Mythread02 extends Thread{ private static int count = 0; @Override public void run() { for (int i = 0; i < 100; i++) { add(); } } synchronized public void add(){ count++; } public static int getCount(){ return count; }}運行輸出的結果還是會小于 10000。
分析: synchronized 加在非 static 方法上意思是鎖住當前對象,而多個線程使用的是多個對象,一個線程進來后出來之前,另一個線程還是會進來的。
對策: 將 add() 改為靜態方法,保證所有線程在這個方法上使用的是同一個對象鎖。
synchronized static public void add(){ count++;}結論: synchronized 能夠保證變量的同步性;但是一定要注意,同步方法或同步塊一定要使用同一個對象鎖。
我們回到文章開頭的死循環的例子; 除了使用 volatile 修飾共享變量能保證可見性之外,synchronized 同樣可以實現。 我們可以這樣改:
public class VolatileExample01 { public static void main(String[] args) throws Exception { MyThread thread = new MyThread(); thread.start(); try { Thread.sleep(3000); } finally { thread.setFlag(true); } }}class MyThread extends Thread { private boolean flag = false; @Override public void run() { while (!flag) { synchronized (this) { // do something ... } } } public void setFlag(boolean flag) { this.flag = flag; }}分析:

結論: synchronized 也是可以實現變量對所有線程可見的。
原理:【附錄】鎖的內存語義。

volatile 的讀內存語義:當讀一個 volatile 變量時,JMM 將該線程對應的本地內存置為無效,從主內存中讀取變量; volatile 的寫內存語義:當寫一個 volatile 變量時,JMM 將該線程對應的本地內存中的共享變量值刷新到主內存。
什么是內存屏障:用于實現對內存操作的順序限制。

為了實現 volatile 的內存語義,JMM 會限制重排序:
限制 volatile 修飾的共享變量之間的重排序;限制 volatile 修飾的共享變量與普通共享變量之間的重排序;下面是 JMM 制定的 volatile 重排序規則表:

JMM 通過插入內存屏障的方式限制重排序,如下圖:


詳解:
在每個 volatile 寫操作前插入 StoreStore 屏障在每個 volatile 寫操作后插入 StoreLoad 屏障在每個 volatile 讀操作后插入 LoadLoad 屏障在每個 volatile 讀操作后插入 LoadStore 屏障注意:加鎖解鎖必須是同一把鎖。
可以看出,鎖釋放和 volatile 讀具有相同的內存語義;鎖獲取和 volatile 寫具有相同的內存語義
內置鎖,即使用 synchronized 形成的鎖。
內置鎖依賴于 JVM。編譯器會在同步塊的入口位置和退出位置分別插入 monitorenter 和 monitorexit字節碼指令。而對于 synchronized 方法,編譯器會在 Class 文件的方法表中將該方法的 access_flags 字段中的 synchronized 標志位置 1,表示該方法是同步方法并使用調用該方法的對象。
以 ReentrantLock(分為公平鎖和非公平鎖) 為例:
公平鎖 加鎖:首先會調用 getState() 方法讀 volatile 變量 state; 解鎖:setState(int newState) 方法寫 volatile 變量 state。 實質上還是在使用 volatile 共享變量。
非公平鎖 加鎖:首先會使用 CAS 更新 volatile 變量 state,更新不成功再去采用公平鎖的方式(比較粗魯) 解鎖:setState(int newState) 方法寫 volatile 變量 state。 CAS 先讀后寫,CAS 讀(volatile 讀)不會與后面的任何操作重排序,CAS 寫(volatile 寫)不會與前面的任何操作重排序,所以 CAS 操作不會與 CAS 前面和后面的任意操作重排序。
利用了 CAS 附帶 volatile 變量實現。
Q:為什么 volatile 寫只加入了Store-Store屏障呢,這樣普通讀不就可以重拍到volatile寫的下方了?
新聞熱點
疑難解答