深入理解volatile關鍵字
1.volatile與可見性
都知道volatile可以保證可見性,那到底是如何保證的呢?
這便於Happen-before原則有關,該原則的第三條規定:對一個volatile修飾的變量,寫入操作要早於對這個變量的讀取操作。具體步驟如下:
A執行緒將共享變數讀進工作記憶體中,同時B執行緒也會將共享變數讀進工作記憶體中。
在A執行緒對共享變數修改後,會立即刷新到主內存,此時B線程的工作內存中的共享變數就會被設定無效,需要從主內存重新讀取新值。反映到硬體上就是CPU的Cache line 置為無效狀態。
這樣便保證了可見性,簡單而言,就是線程在對volatile修飾的變量修改且刷新到主內存之後,會使得其它線程的工作內存中的共享變量無效,需要從主內存中再此讀取。
2.volatile與有序性
都知道volatile可以保證有序性,那麼到底是如何保證的呢?
volatile保證有序性,比較直接,禁止JVM和處理器對volatile關鍵字修飾的變數進行指令重排序,但對於該變數之前或之後的可以任意排序,只要最終的結果與沒更改前的結果保持一致即可。
底層原理
被volatile修飾的變數在底層會加上一個「lock:」的前綴,「lock」字首的指令相當於記憶體屏障,這正是保證可見性與有序性的關鍵,而這個屏障的作用主要有一下幾點:
指令重排時,屏障前的程式碼不能重排到屏障後,屏障後的也不能重排到屏障前。
執行到記憶體屏障時,確保前面的程式碼都已經執行完畢,且執行結果是對屏障後的程式碼可見的。
強制將工作記憶體中的變數刷新到主記憶體。
其它執行緒的工作記憶體的變數會設定無效,需要重現從主記憶體讀取。
3.volatile與原子性
都知道volatile不能保證原子性,那為何不能保證原子性呢?
程式碼示範:
package com.github.excellent01; import java.util.concurrent.CountDownLatch; /** * @auther plg * @date 2019/5/19 9:37 */ public class TestVolatile implements Runnable { private volatile Integer num = 0; private static CountDownLatch latch = new CountDownLatch(10); @Override public void run() { for(int i = 0; i < 1000; i++){ num++; } latch.countDown(); } public Integer getNum() { return num; } public static void main(String[] args) throws InterruptedException { TestVolatile test = new TestVolatile(); for(int i = 0; i < 10; i++){ new Thread(test).start(); } latch.await(); System.out.println(test.getNum()); } }
啟動10個線程,每個線程對共享變數num,加1000次,當所有的線程執行完畢之後,列印輸出num 的最終結果。
#很少有10000的這便是因為volatile無法保證原子性造成的。
原因分析:
num 的操作由三個步驟組成:
從主記憶體將num讀進工作內存中
在工作記憶體中進行加一
加一完成後,寫回主記憶體。
雖然這三個步驟都是原子操作,但合起來不是原子操作,每一步執行的過程中都有可能被打斷。
假設此時num的值為10,線程A將變數讀進自己的工作內存中,此時發生了CPU切換,B也將num讀進自己的工作內存,此時值也是10 .B線程在自己的工作內存中對num的值進行修改,變成了11,但此時還沒有刷新到主內存,因此A線程還不知道num的值已經發生了改變,之前所說的,修改volatile變數後,其它線程會立即得知,前提也是要先刷新到主記憶體中,這時,其它線程才會將自己工作中的共享變數的值設為無效。因為沒有刷新到主內存,因此A傻傻的不知道,在10的基礎上加一,因此最終雖然兩個線程都進行了加一操作,但最終的結果只加了一次。
這便是為什麼volatile不能保證原子性。
volatile的使用場景
根據volatile的特點,保證有序性,可見性,不能保證原子性,因此volatile可以用於那些不需要原子性,或者說原子性已經得到保障的場合:
代碼演示
volatile boolean shutdownRequested public void shutdown() { shutdownRequested = true; } public void work() { while(shutdownRequested) { //do stuff } }
只要線程對shutdownRequested進行修改,執行work的線程會立即看到,因此會立即停止下來,如果不加volatile的話,它每次去工作記憶體中讀取資料一直是個true,一直執行,都不知道別人已經讓它停了。
程式碼示範:
package com.github.excellent; import java.util.concurrent.ThreadPoolExecutor; /** * 启动线程会被阻塞,flag 从内存读入,会存入寄存器中,下次直接从寄存器取值 * 因此值一直是false * 即使别的线程已经将值更改了,它也不知道 * 加volatile即可。也可以加锁,只要保证内存可见性即可 * @auther plg * @date 2019/5/2 22:40 */ public class Testvolatile { public static boolean flag = false; public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ for(;;) { System.out.println(flag); } }); Thread thread2 = new Thread(()->{ for(;;){ flag = true; } }); thread1.start(); Thread.sleep(1000); thread2.start(); } }
執行結果:
就是這麼笨,別人修改了自己不知道,還輸出false。加一個volatile就ok了。
以上是深入理解volatile關鍵字的詳細內容。更多資訊請關注PHP中文網其他相關文章!