volatile原理技術知識整理

volatile是一種輕量且在有限的條件下線程安全技術,它保證修飾的變量的可見性和有序性,但非原子性。相對於synchronize高效,而經常跟synchronize配合使用。
volatile原理.png

一. Java內存模型

這裏主要描述的線程,工做內存,主存的變量的讀寫關係:java

  1. 主存存放線程須要操做的變量,但線程並不直接操做主存。
  2. 每一個線程讀取主存變量都是先拷貝一份到工做內存中,不一樣線程工做內存互不干擾。
  3. 線程修改了工做內存後,再寫回主存中。
  4. 每次從主存讀寫的過程都須要通過8原子性操做。

java內存模型.png

二. volatile可見性

1. volatile特殊性

(1) 操做use以前必須先執行read和load操做。安全

(2) 操做assign以後必須執行store和write操做。多線程

由特性性保證了read、load和use的操做連續性,assign、store和write的操做連續性,從而達到工做內存讀取前必須刷新主存最新值;工做內存寫入後必須同步到主存中。讀取的連續性和寫入的連續性,看上去像線程直接操做了主存ide

擴展: lock和unlock操做並不直接開放給用戶使用,而是提供給像Synchronize關鍵字指定monitorenter和monitorexit隱式使用。關於Synchronize的監聽器鎖monitor,javac編譯後會在做用的方法先後增長monitorenter和monitorexit指令,詳細的能夠查看Synchronize原理。性能

2. 代碼驗證可見性
public class VolatileVisibility {

public static class TestData {
	volatile int num = 0;
	public void updateNum(){
		num = 1;
	}
}

public static void main(String[] args) {
	final TestData testData = new TestData();
	new Thread(new Runnable() {
		
		@Override
		public void run() {
			System.out.println("ChildThread num-->"+testData.num);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			testData.updateNum();
			System.out.println("ChildThread update num-->"+testData.num);
		}
	}).start();
	
	while (testData.num == 0){
     }
	
	System.out.println("MainThread num-->"+testData.num);
  }
}
複製代碼

(1) TestData中的num不添加volatile關鍵字,System.out.println("MainThread num-->"+testData.num);這一句一直不會執行。表示while中的條件testData.num == 0一直爲0,子線程修改了num對主線程不起做用。優化

(2) TestData中的num添加volatile關鍵字,System.out.println("MainThread num-->"+testData.num);會執行,結果以下。spa

ChildThread num-->0
ChildThread update num-->1
MainThread num-->1
複製代碼

三. volatile非原子性

1. use和assign這兩個操做總體上不是一個連續性的原子操做。

volatile自己並不對數據運算處理維持原子性,強調的是讀寫及時影響主存。線程

2. 非原子性操做

volatile修飾num,num++;num = num+1;這種就是非原子性操做。指針

(1) 主存讀取num的值;code

(2) 進行num++運算;

(3) 將num值寫到主存。

像種操做在多線程環境中,use和assign是屢次出現,若是有兩個線程中讀取到主存的num都是2,且同時執行num++,兩個線程的結果都是3,這樣就產生了髒數據,再寫入主存中都是3。核心num++運算並沒保證前後順序執行。爲了保證執行運算的線程順序,能夠選擇Synchronize。

3. 代碼驗證非原子性
public class ValatileAtomic {
public static class TestData {
	volatile int num = 0;
      //synchronized
	public void updateNum(){
		num++;
	}
}

public static void main(String[] args) {
	final TestData testData = new TestData();
	for(int i = 1; i <= 10; i++) {
        new Thread(new Runnable() {
			@Override
			public void run() {
				for (int j = 1; j <= 1000; j++) {
					testData.updateNum();
                }
			}
		}).start();
    }
    
    while (Thread.activeCount() > 2) {
        Thread.yield();
    }
    System.out.println("最終結果:" + testData.num);
  }
}
複製代碼

按咱們的意願10個線程,每一個線程累加線程累加1000,一共是10 * 1000=10000。可是volatile int num = 0; 使用volatile與否都是體現非原子性,運行的結果都比10000小:

最終結果:9701
複製代碼

爲了實現同步操做,在方法updateNum()前添加關鍵字synchronize便可:

最終結果:10000
複製代碼

四. volatile有序性

1. volatile禁止指令重排

(1) 指令重排:爲了提升性能,編譯器和和處理器一般會對指令進行指令重排序。

指令重排.png
圖中的三個重排位置能夠調換的,根據系統優化須要進行重排。遵循的原則是單線程重排後的執行結果要與順序執行結果相同。

(2) 內存屏障指令:volatile在指令之間插入內存屏障,保證按照特定順序執行和某些變量的可見性。

volatile就是經過內存屏障通知cpu和編譯器不作指令重排優化來維持有序性。

2. synchronize串行控制

(1) synchronize無禁止指令重排。

(2) 一個變量在同一時刻只容許一條線程對其進行lock操做,獲取對象鎖,互斥排他性達到兩個同步塊串行執行。

五. volatile線程安全適用範圍

因爲volatile的非原子性緣由,因此它的線程安全是有條件的:

(1) 運算結果不依賴但前置,或者能保證自由一個單一線程修改變量值。

(2) 變量不須要與其餘的狀態變量共同參與不變的約束。 這兩條件描述出自於《深刻理解java虛擬機》。

六. volatile與synchronize配合使用

1. DCL單例代碼
public class Singleton {

private volatile static Singleton instance = null;
private Singleton(){
	
}

public static Singleton getInstance(){
	if(instance == null){ // 第①處
		synchronized (Singleton.class) {
			if(instance == null){  // 第②處
				instance = new Singleton();
			}
		}
	}
	return instance;
  }
}
複製代碼
2.爲何還要使用volatile來修飾

按照上邊的寫法已經對new Singleton();這個操做進行了synchronize操做,已經保證了多線程只能串行執行這個實例化代碼。事實上,synchronize保證了線程執行實例化這段代碼是串行的,可是Synchronize並不具有禁止指令重排的特性。

instance = new Singleton(); 主要作了3件事情:

(1) java虛擬機爲對象分配一塊內存x。

(2) 在內存x上爲對象進行初始化 。

(3) 將內存x的地址賦值給instance 變量。

若是編譯器進行重排爲:

(1) java虛擬機爲對象分配一塊內存x。

(2) 將內存x的地址賦值給instance 變量。

(3) 在內存x上爲對象進行初始化 。

第一種狀況,無volatile修飾:此時,有兩個線程執行getInstance()方法,加入線程A進入代碼的註釋中的第②處,並執行到了重排指令的(2),與其同時線程B恰好代碼註釋中的第①處的if判斷。此時,instance有線程A把內存地址x地址賦值給了instance,那麼instance已經不爲空只是沒有初始化完成,線程B就返回了一個沒有完成初始化的instance,最終使用時候會出現空指針的錯誤。

第二種狀況,有volatile修飾:instance由於被volatile的禁止指令重排的特性,那隻會安裝先初始化對象再賦值給instance這樣順序執行,這樣就能保證返回正常的實例化的對象。

七. 小結

1.volatile具備可見性和有序性,不能保證原子性。

2.volatile在特定狀況下線程安全,好比自身不作非原子性運算。

3.synchronize經過獲取對象鎖,保證代碼塊串行執行,無禁止指令重排能力。

4.DCL單例操做須要volatile和synchronize保證線程安全。

相關文章
相關標籤/搜索