volatile 是一個類型修飾符,使用方式以下
private volatile int a = 0;
java
原子性
一個或者多個操做,要麼所有執行而且中途不能被打斷,要麼都不執行。安全
可見性
同一個線程裏,先執行的代碼結果對後執行的代碼可見,不一樣線程裏任意線程對某個變量修改後,其它線程可以及時知道修改後的結果。多線程
有序性
同一線程裏,程序的執行順序按照代碼的前後順序執行。併發
只有知足了以上三個前提,才能說線程是安全的性能
volatile關鍵字在多線程中,只保證可見性、有序性。但不保證原子性。測試
來看一個網上找的例子線程
public class TestVolatele { //測試一 private static boolean isOk = true; //測試二 //private static volatile boolean isOk = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "--開始循環了"); while (isOk) { } System.out.println(Thread.currentThread().getName() + "--跳出循環了"); }, "t1").start(); new Thread(() -> { try { Thread.sleep(2000); } catch (Exception e) { } isOk = false; System.out.println(Thread.currentThread().getName() + "--isOk改成 false"); }, "t2").start(); } }
這個例子很簡單,t1線程會一直監聽isOk字段,t2線程負責修改isOk字段,正常狀況下,當t2把isOk改成false時,t1應該會退出while循環,運行代碼來驗證一下結果。code
上圖是沒有加volatile關鍵字的運行結果。能夠看到線程 t1 並無執行完。
這說明線程 t2 修改了 isOk = false 以後,在線程 t1 中並不知道該字段被修改了。blog
上圖加volatile關鍵字的運行結果。線程t1也執行完了。
能夠看出加了volatile關鍵字,那麼isOk就具有了可見性。排序
爲了解釋可見性的緣由,能夠看上圖的java內存模型圖。isOk = true這個字段其實時存在主內存中的。當線程要操做isOk時,先把isOk複製一份到本身的工做內存中,在工做內存中對字段操做完後,會再把字段寫入主內存。
假設沒有volatile字段時
一、t1 線程從主內存複製 isOk = true 到 t1 工做內存
二、t2 線程從主內存複製 isOk = true 到 t2 工做內存
三、t2 修改工做內存 isOk = false,並賦值給主內存(主內存isOk = false)
四、t1 線程讀取的仍是 t1 工做內存(isOk= true),並不知道主內存isOk已改成false
五、因爲沒法實時獲取主內存最新數據,因此致使一直while循環
有volatile字段時
一、t1 線程從主內存複製 isOk = true 到 t1 工做內存
二、t2 線程從主內存複製 isOk = true 到 t2 工做內存
三、t2 修改工做內存 isOk = false,並賦值給主內存(主內存isOk = false)
四、isOk 因爲加了volatile關鍵字,這時 t1 線程強制讀取主內存數據
五、讀取到主內存isOk=false,退出while循環(能夠理解爲volatile關鍵字對主內存保證可見性)
什麼是有序性?咱們寫的Java程序代碼不老是按順序執行的,都有可能出現程序重排序(指令重排)的狀況,這麼作的好處就是爲了讓執行塊的程序代碼先執行,執行慢的程序放到後面去,提升總體運行效率。
int a = 1; int b = 2;
上述的兩條賦值語句在程序運行時,並不必定按照順序先給a賦值,而後再給b賦值,頗有可能先執行b再執行a,這是程序爲了提升效率出現了指令重排序。雖然在單個線程中,指令重排序不會對結果產生任何問題,可是在多線程中出現指令重排序,可能會致使最終的結果不是咱們想要的。
舉例個單例模式(懶漢式)的例子
public class Singleton { private volatile static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }
若是上邊的代碼不使用volatile關鍵字,可能會出現問題。問題在於 instance = new Singleton();這行代碼,其實這行代碼能夠拆分爲
一、爲instance分配內存
二、初始化instance
三、將instance變量指向分配的內存空間
現有A和B兩條線程同時調用 getInstance() 方法,假設A線程先執行了instance = new Singleton() 而且發生了指令重排序。可能會出現A線程先執行第三步,後執行第二步的狀況。也就是說可能會出現instance變量還沒初始化完成,B線程就已經判斷了該變量值不爲null,結果返回了一個沒有初始化完成的半成品的狀況。因此在單例的懶漢式中須要加上volatile關鍵字禁止指令重排序
public class TestVolatele { private static volatile long n = 0; public static void main(String[] args) throws Exception { List<Thread> tList = new ArrayList<>(); for (int i = 0; i < 5; i++) { tList.add(new Thread(() -> { for (int j = 0; j < 2000; j++) { n++; } })); } for (Thread thread : tList) { thread.start(); } for (Thread thread : tList) { thread.join(); } System.out.println(n); } }
上面的代碼開啓5條線程,每條線程對n++兩千次。若是volatile關鍵字具有原子性,那麼結果確定等於10000。但實際上每次運行的結果都不一樣,結果中n老是 <= 10000,這說明volatile不具有原子性。
但可能會疑惑,volatile關鍵字不是直讀取主內存嗎?明明能夠實時拿到到主內存的最新數據,爲何還不保證原子性?這就須要把n++給拆分來解釋
能夠把n++拆分爲3個階段
一、讀取 n
二、對 n 加 1
三、把 n 寫入主內存
把n++拆分以後,再來分析結果 n <= 10000 的緣由
假設A、B兩條線程操做 n++,而且n初始值爲0
一、A 加載主內存 n=0 到 A 工做內存
二、B 加載主內存 n=0 到 B 工做內存
三、A 在工做內存中執行 n++ ,把結果 n=1 寫入主內存
四、B 強制讀取主內存 n = 1 ,並執行 n++ 操做(到這裏都沒問題)
五、可是,B 在執行 n++ 時,只執行了 n++ 的第1步,讀取 n=1(這時 B 就中止了,CPU切換到A線程執行)
六、此時線程 A 執行了 n++ ,而且執行完了,把結果寫入主內存 n=2
七、CPU又切換到 B 執行了,B 執行 n++ 的第2步,對n加1。(此時 B 中 n=2)
八、B 執行完後,把 n=2 寫入主內存,這就致使了主內存中寫入了兩次 n=2
上邊的舉例因爲主內存寫入兩次 n=2,因此最終致使 n <= 10000 的,因此說 volatile 不保證原子性。 volatile 它只針對讀取時可見,既讀取時的數據保證最新的,可是並不保證寫入數據時不存在問題。
volatile的應用要從它的特性入手,只保證可見性、有序性。但不保證原子性。
(1)volatile最適合使用的地方是一個線程寫、其它線程讀的場合,若是有多個線程併發寫操做,仍然須要使用鎖或者線程安全的容器或者原子變量來代替。 (2)假如一個線程寫、一個線程讀,根據前面針對volatile的應用總結,此時可使用volatile來代替傳統的synchronized關鍵字提高併發訪問的性能。 (3)volatile不適合多個線程同時寫的狀況,由於volatile不保證原子性,多線程同時寫會有問題