volatile關鍵字的2個作用
1.線程的可見性
2.防止指令重排
什么是線程的可見性?
線程的可見性 就是一個線程對一個變量進行更改操作 其他線程獲取會獲得最新的值。
線程在執行的行 操作主線程的變量。會將變量的副本拷貝一份到線程的工作區域(避免每次到主線程讀取 提高效率),在更改后的一段時間內寫入主內存
如下示例代碼:
public class Accounting implements Runnable { boolean quit=false; int i=0; @Override public void run() { while (!quit){ i++; } System.out.println("線程退出"); } public static void main(String[] args) throws InterruptedException { Accounting accounting = new Accounting(); Thread a1 = new Thread(accounting, "a1"); Thread a2 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); System.out.println("開始通知線程結束"); accounting.setQuit(true); } catch (InterruptedException e) { e.printStackTrace(); } } }); a2.start(); a1.start(); Thread.sleep(1000); } public boolean isQuit() { return quit; } public void setQuit(boolean quit) { this.quit = quit; } }
這段代碼的邏輯就是線程a1 執行循環操作 a2 2秒后設置quit為true任務結束 打印 "線程退出";
那么真的能夠成功退出嗎?我們看看 線程執行在內存中的操作圖

打印:
開始通知線程結束
a2 線程首先將自己工作線程的quit改為ture ,然后一定時間之后去將主內存的quit改為true ,但是a1線程始終是操作的是自己的工作內存的副本 所以死循環
這個時候在quit加上volatile關鍵字
volatile boolean quit=false;
打印
開始通知線程結束
線程退出
加上volatile關鍵字后。當一個線程對變量進行修改會更新自己的工作內存里面的值,然后立即將改動的值刷新到主內存,同時線程2的工作內存的quit副本緩存失效 下次直接到主內存讀取 所以能夠正常執行
記錄一個小插曲
System.out.println,sychronized,Thread.sleep Thread.sleep 影響可見性?
System.out.println
public class Accounting implements Runnable { boolean quit=false; int i=0; @Override public void run() {
while (!quit){ i++; System.out.println(i); } System.out.println("線程退出"); } public static void main(String[] args) throws InterruptedException { Accounting accounting = new Accounting(); Thread a1 = new Thread(accounting, "a1"); Thread a2 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); System.out.println("開始通知線程結束"); accounting.setQuit(true); } catch (InterruptedException e) { e.printStackTrace(); } } }); a2.start(); a1.start(); Thread.sleep(1000); } public boolean isQuit() { return quit; } public void setQuit(boolean quit) { this.quit = quit; }
會發現沒有加上volatile一樣可以成功退出 。那我們上面說的 線程的內存處理 不成立了嗎?
查資料說 是因為jvm對鎖的優化。因為如果我們在循環里面加上sychronize同步鎖 會產生大量的鎖競爭 所以jvm優化過后
synchronized (this){ while (!quit){ //..... } }
但是我們並沒有在while里面加鎖啊。我們看看打印的方法源碼
public void println(int x) { synchronized (this) { print(x); newLine(); } }
sleep方法並沒有加鎖,為什么能夠保證可見性
sleep是阻塞線程並不釋放鎖,讓出cpu調度。 讓出cpu調度后下次執行會刷新工作內存
指令重排
指令重排指在編譯的時候,在不單線程運行不影響結果的情況下進行指令優化
如:
public class Context { boolean isLoad=false; Object configuration=null; public void loadConfiguration(){ System.out.println("正在加載配置文件"); configuration= new Object(); isLoad=true; } public void initContext(){ System.out.println("正在進行初始化"); } public static void main(String[] args) { Context context=new Context(); context.loadConfiguration(); if(context.isLoad){ context.initContext(); } } }
這段代碼就是先加載配置文件信息 然后初始化上下文
我們在單線程下 把他們的順序調換模擬指令重排 會對結果沒有影響
public void loadConfiguration(){ isLoad=true; System.out.println("正在加載配置文件"); configuration= new Object(); }
但是在多線程下面
public class Context { boolean isLoad=false; Object configuration=null; public void loadConfiguration(){ //模擬jvm指令重排 將isLoad命令排在第一位 isLoad=true; /*** * 模擬並發情況下指令重排。導致的isload=true排到前面。 * 這個時候配置文件沒初始化。initContext監聽到lsLoad等於true根據配置文件進行初始化 */ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } configuration= new Object(); //isLoad=true;指令重排前 } public void initContext(){ configuration.toString(); System.out.println("正在進行初始化"); } public static void main(String[] args) { Context context=new Context(); //負責監聽 如果加載完畢 則進行上下午初始化 Thread t2=new Thread(new Runnable() { @Override public void run() { while (true){ if(context.isLoad){ context.initContext(); break; } } } },"t2"); //負責加載配置文件 Thread t1=new Thread(new Runnable() { @Override public void run() { context.loadConfiguration(); } },"t1"); t1.start(); t2.start(); } }
只是模擬指令重排 先不考慮可見性 這種情況會初始化context 沒有configuration 報錯 使用volatile關鍵字修飾可以避免
值得注意的一點
volatile雖然能夠保證線程的可見性 但是並不能保證原子性 比如i++操作 都是讀出i的值 進行運算再寫入。如果在讀出的時候別的線程改變了 就會不一致
哪種場景適合用volatile 對一個變量的值進行修改 不依賴其他值。 比如 index=true 而不是i=i+j;或則index=j>a 或 a=j (會從內存中讀出j的值 然后賦值到a);
java提供atomic cas能夠性能比鎖高能夠保證原子性 如:atomicInt atomictDouble
