深刻理解java併發編程基礎篇(三)-------volatile

1、前言

  在上一篇,咱們研究了Java內存模型,而且知道Java內存模型的概念以及做用,圍繞着原子性、可見性、有序性進行了簡單的概述,那麼在這一篇咱們首先會介紹volatile關鍵字的基礎認知,而後深刻的去解析volatile在這三個特性中究竟有什麼樣的做用?volatile是如何實現的?java

2、volatile的用法

  volatile一般被比喻成」輕量級的Synchronized「,也是Java併發編程中比較重要的一個關鍵字。和Synchronized不一樣,volatile是一個變量修飾符,只能用來修飾變量。沒法修飾方法及代碼塊等。c++

  volatile的用法比較簡單,只須要在聲明一個可能被多線程同時訪問的變量時,使用volatile修飾就能夠了。算法

  舉一個單例實現的簡單例子,代碼以下:編程

package com.MyMineBug.demoRun.test;

public class Singleton {

	private volatile static Singleton singleton;

	private Singleton() {
		
	};

	public static Singleton getInstance() {
		if(singleton == null) {
			synchronized (Singleton.class) {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}

}
複製代碼

這段代碼是比較典型的使用雙重鎖校驗實現單例的一種形式,其中使用volatile關鍵字修飾能夠被多個線程同時訪問。緩存

3、volatile的特性

  首先看一個代碼例子:多線程

package com.MyMineBug.demoRun.test;

class VolatileFeaturesExample {

    volatile long vl = 0L;  // 使用 volatile 聲明 64 位的 long 型變量 

    public void set(long l) {
        vl = l;   // 單個 volatile 變量的寫 
    }

    public void getAndIncrement () {
        vl++;    // 複合(多個)volatile 變量的讀 / 寫 
    }

    public long get() {
        return vl;   // 單個 volatile 變量的讀 
    }
}
複製代碼

假設有多個線程分別調用上面程序的三個方法,這個程序在語意上和下面程序等價:併發

package com.MyMineBug.demoRun.test;

class VolatileFeaturesExample {
    long vl = 0L;               // 64 位的 long 型普通變量 

    public synchronized void set(long l) {     // 對單個的普通 變量的寫用同一個監視器同步 
        vl = l;
    }

    public void getAndIncrement () { // 普通方法調用 
        long temp = get();           // 調用已同步的讀方法 
        temp += 1L;                  // 普通寫操做 
        set(temp);                   // 調用已同步的寫方法 
    }
    
    public synchronized long get() { 
    // 對單個的普通變量的讀用同一個監視器同步 
        return vl;
    }
}
複製代碼

如上面示例程序所示,對一個 volatile 變量的單個讀 / 寫操做,與對一個普通變量的讀 / 寫操做使用同一個監視器鎖來同步,它們之間的執行效果相同。 經過對比,咱們能夠知道:post

1.可見性。對一個 volatile 變量的讀,老是能看到(任意線程)對這個 volatile 變量最後的寫入。優化

2.原子性:對任意單個 volatile 變量的讀 / 寫具備原子性,但相似於 volatile++ 這種複合操做不具備原子性。spa

3.1 volatile與有序性

  volatile一個強大的功能,那就是他能夠禁止指令重排優化。經過禁止指令重排優化,就能夠保證代碼程序會嚴格按照代碼的前後順序執行。那麼volatile又是如何禁止指令重排的呢?

  先看一個概念內存屏障(Memory Barrier):是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操做中的一個同步點,使得此點以前的全部讀寫操做都執行後才能夠開始執行此點以後的操做。而volatile就是是經過內存屏障來禁止指令重排的。下表描述了和volatile有關的指令重排禁止行爲:

從上表咱們能夠看出:

當第二個操做是volatile寫時,無論第一個操做是什麼,都不能重排序。這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後。

當第一個操做是volatile讀時,無論第二個操做是什麼,都不能重排序。這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。

當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序。

爲了實現 volatile 的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對於編譯器來講,發現一個最優佈置來最小化插入屏障的總數幾乎不可能,爲此,JMM 採起保守策略。下面是基於保守策略的 JMM 內存屏障插入策略:

在每一個 volatile 寫操做的前面插入一個 StoreStore 屏障。

在每一個 volatile 寫操做的後面插入一個 StoreLoad 屏障。

在每一個 volatile 讀操做的後面插入一個 LoadLoad 屏障。

在每一個 volatile 讀操做的後面插入一個 LoadStore 屏障。

這種保守策略總結以下:

接下來,咱們經過具體的代碼來講明:

package com.MyMineBug.demoRun.test;

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           // 第一個 volatile 讀 
        int j = v2;           // 第二個 volatile 讀 
        a = i + j;            // 普通寫 
        v1 = i + 1;          // 第一個 volatile 寫 
        v2 = j * 2;          // 第二個 volatile 寫 
    }

    …                    // 其餘方法 
}
複製代碼

針對 readAndWrite() 方法,編譯器在生成字節碼時能夠作以下的優化:

因此,volatile經過在volatile變量的操做先後插入內存屏障的方式,來禁止指令重排,進而保證多線程狀況下對共享變量的有序性。

3.2 volatile與可見性

  可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。

  在上一篇文章深刻理解java併發編程基礎篇(二)-------線程、進程、Java內存模型中,咱們知道:Java內存模型規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程的工做內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量的傳遞均須要本身的工做內存和主存之間進行數據同步進行。因此,就可能出現線程1改了某個變量的值,可是線程2不可見的狀況。

  在Java中,咱們知道被volatile修飾的變量在被修改後能夠當即同步到主內存,被其修飾的變量在每次是用以前都從主內存刷新。所以,可使用volatile來保證多線程操做時變量的可見性。那麼被volatile修飾的變量程序是如何讓具體保證其可見性呢?這就與*內存屏障有關。
  volatile對於可見性的實現,內存屏障也起着相當重要的做用。由於內存屏障至關於一個數據同步點,他要保證在這個同步點以後的讀寫操做必須在這個點以前的讀寫操做都執行完以後才能夠執行。而且在遇到內存屏障的時候,緩存數據會和主存進行同步,或者把緩存數據寫入主存、或者從主存把數據讀取到緩存。

  因此,內存屏障也是保證可見性的重要手段,操做系統經過內存屏障保證緩存間的可見性,JVM經過給volatile變量加入內存屏障保證線程之間的可見性。

3.3 volatile與原子性

  原子性是指一個操做是不可中斷的,要麼所有執行完成,要麼就都不執行。

  在咱們的實際應用場景中,咱們應該知道的是:volatile是不能保證原子性的。 那麼爲神馬volatile是不能保證原子性?

  下一篇介紹synchronized的時候,咱們會知道爲了保證原子性,須要經過字節碼指令monitorenter和monitorexit,可是volatile和這兩個指令之間是沒有任何關係的。

  根據本身的理解是:線程是CPU調度的基本單位。CPU有時間片的概念,會根據不一樣的調度算法進行線程調度。當一個線程得到時間片以後開始執行,在時間片耗盡以後,就會失去CPU使用權。因此在多線程場景下,因爲時間片在線程間輪換,就會發生原子性問題。

下面來看一段volatile與原子性的代碼:

package com.MyMineBug.demoRun.test;

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
複製代碼

以上代碼比較簡單,就是建立10個線程,而後分別執行1000次 i++ 操做。正常狀況下,程序的輸出結果應該是10000,可是,屢次執行的結果都小於10000。這其實就是volatile沒法知足原子性的緣由。
爲何會出現這種狀況呢,那就是由於雖然volatile能夠保證inc在多個線程之間的可見性。可是沒法inc++ 的原子性。

4、總結

  volatile有序性和可見性是經過內存屏障實現的。而volatile是沒法保證原子性的。 在下一下篇,咱們將深刻解析關鍵字synchronized

  若是以爲還不錯,請點個贊!!!

  Share Technology And Love Life

相關文章
相關標籤/搜索