volatile

volatile是什麼

volatile 是一個類型修飾符,使用方式以下
private volatile int a = 0;java

線程安全的前提

  • 原子性
    一個或者多個操做,要麼所有執行而且中途不能被打斷,要麼都不執行。安全

  • 可見性
    同一個線程裏,先執行的代碼結果對後執行的代碼可見,不一樣線程裏任意線程對某個變量修改後,其它線程可以及時知道修改後的結果。多線程

  • 有序性
    同一線程裏,程序的執行順序按照代碼的前後順序執行。併發

只有知足了以上三個前提,才能說線程是安全的性能

volatile的做用

volatile關鍵字在多線程中,只保證可見性、有序性。但不保證原子性。測試

1. 保證可見性

來看一個網上找的例子線程

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關鍵字對主內存保證可見性)

2.保證有序性(禁止指令重排序)

什麼是有序性?咱們寫的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關鍵字禁止指令重排序

3.不保證原子性

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應用場景

volatile的應用要從它的特性入手,只保證可見性、有序性。但不保證原子性。

(1)volatile最適合使用的地方是一個線程寫、其它線程讀的場合,若是有多個線程併發寫操做,仍然須要使用鎖或者線程安全的容器或者原子變量來代替。 (2)假如一個線程寫、一個線程讀,根據前面針對volatile的應用總結,此時可使用volatile來代替傳統的synchronized關鍵字提高併發訪問的性能。 (3)volatile不適合多個線程同時寫的狀況,由於volatile不保證原子性,多線程同時寫會有問題

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息