Java多線程之三volatile與等待通知機制示例

原子性,可見性與有序性

在多線程中,線程同步的時候通常須要考慮原子性,可見性與有序性java

原子性

原子性定義:一個操做或者多個操做在執行過程當中要麼所有執行完成,要麼所有都不執行,不存在執行一部分的狀況。安全

以咱們在Java代碼中常常用到的自增操做i++爲例,i++實際上並非一步操做,而是首先對i的值加一,而後將結果再賦值給i。在單線程中不會存在問題,但若是在多線程中咱們考慮這樣一個狀況:i是一個共享變量,初始值爲0,假設線程一以執行到某一步正好進行自增操做i++,恰好對i進行了加一可是還沒將值從新賦給i,此時當前線程被cpu掛起,而另外一個線程二開始執行,恰好也對i進行了一個賦值操做i=10;,等線程一從新執行後會將i自增後的值1賦給i,此時至關於覆蓋了線程二的賦值操做。此時將會產生線程不安全的狀況。bash

可見性

多個線程同時訪問一個共享的變量的時候,每一個線程的工做內存有這個變量的一個拷貝,變量自己仍是保存在共享內存(堆)中。因此並非每一次一個線程修改了值後其餘線程均可以當即取到修改後的值。可見性是指當其餘的線程訪問同一個變量時,當一個線程修改了這個變量的值,其餘線程也可以當即看獲得修改的值。多線程

有序性

有序性是指程序的執行嚴格按照咱們寫的代碼的順序進行執行。dom

指令重排

通常狀況下,CPU和編譯器爲了提高程序執行的效率,會按照必定的規則容許對指令進行優化,即調整實際指令運行的順序。指令重排不會對單線程的程序形成任何不利的影響,可是多線程環境下將會產生一些影響。指令重排的前提條件是指令調整後不會影響單線程程序的執行:ide

int i = 2; //statement 1
int j = 1;//statement 2

int k = i*j;//statement 3
複製代碼

在上面的代碼中對於語句1和語句2相互之間沒有任何依賴,因此可能發生指令重排,可是語句3和語句1,2都有關係,因此語句3必定是在語句1和語句2以後執行的。因此單線程狀況下是絕對不會出現問題的。可是對於多線程可能就發生只是初始化了語句1或者語句2就執行語句3了。測試

要保證在多線程下線程安全,這三大性質都是必需要保證的,而一旦其中一項沒法保證那麼不是線程安全的。前面的synchronized關鍵字就是實現了這三大特性的。優化

volatile

雖然已經有了synchronized關鍵字保證了線程安全須要的三大特性,可是在JDK1.8優化synchronized以前,synchronized關鍵字都是一個重量級的鎖,對程序的效率有着比較大的影響。在java中還有一個synchronized關鍵字的輕量級的實現-volatile關鍵字。volatile關鍵字是在JDK1.5以後從新被重用的一個關鍵字,它能夠保證上訴三大特性中的有序性和可見性,可是不能保證原子性,因此它其實是線程不安全的。ui

保證可見性

出現可見性的緣由在於私有棧幀中的值和公共堆中的共享值不一樣得問題。this

私有棧和共享變量的數據同步

當一個線程在修改普通變量時,其餘線程不能馬上看到修改後的值,若是此時有其餘線程讀取該變量的值,實際上讀到的是沒有修改的值。

volatile關鍵字做用在於當要使用時強制從主內存中讀取值,保證每次讀取的都是公共內存中的值。

讀取公共內存值

防止指令重排

內存屏障也稱爲內存柵欄或柵欄指令,是一種屏障指令,它使CPU或編譯器對屏障指令以前和以後發出的內存操做執行一個排序約束。 這一般意味着在屏障以前發佈的操做被保證在屏障以後發佈的操做以前執行。

volatile關鍵字功能的實現既是經過內存屏障完成的,當使用volatile關鍵字修飾的變量進行讀寫是便會加上內存屏障來保證設計變量的操做順序執行,須要注意的是其和synchronized關鍵字的同步是不同的。

爲何不是原子性的?

實際上volatile關鍵字保證的事全部線程從主存中取到的值是最新的,可是多個線程修改了改變量的值並不會通知其餘線程,除非其餘線程再次從主存中取值。

變量的工做過程

在以上階段中,好比存在兩個線程且兩個線程都已經加載了變量count的值,這時線程一將count修改成10,線程二將值修改成20,可是兩個線程之間並不知道對方都改了值,而最終寫到主存的值也是後寫入的那一個,即始終都一個線程修改的值被覆蓋,因此其並非原子性的。在涉及到多線程操做共享變量是仍是應該加鎖進行操做。

synchronized和volatile關鍵字的比較

  1. volatile關鍵字並非同步操做,其在多線程訪問下不會進行阻塞,而synchronized關鍵字會發阻塞。
  2. volatile關鍵字能保證有序性和可見性,但不能保證原子性。synchronized三種特性都能保證,因此synchronized是線程同步的,而volatile不是。
  3. volatile是線程同步的輕量級實現,因此效率較之synchronized要高。

等待通知機制

在多線程程序中可能會存在多個程序相互配合完成一項功能,這是就須要線程之間進行通訊,在一個線程的工做完成後通知後續線程工做。一般狀況下咱們能夠在一個線程中進行一個while循環操做,設置一個標誌flag,當該線程的前置線程完成後修改flag,後面的while獲得這個標誌後知道自身須要開始工做了,跳出循環。可是這種方法的劣勢在於while循環使得該線程一個須要處於運行中,同時當多個線程相互之間都須要進行通訊時會使得程序變得極其複雜。爲了解決這個問題,有人提出了一種等待通知機制。

等待通知機制是利用JDK中提供的API中的wait()notify/notifyAll()方法來進行實現(實際上Lock類中的方法也能實現),wait()方法是使得當前線程進入等待隊列中,notify/notifyAll()是將等待的線程喚醒。

等待方

  1. 獲取對象鎖
  2. 若是條件不知足,調用對象的wait方法,被通知後依然要檢查條件是否知足
  3. 條件知足之後,才能執行相關的業務邏輯
Synchronized(對象){
	While(條件不知足){
	對象.wait()
}
// do your working
}
複製代碼

通知方

  1. 得到對象的鎖;
  2. 改變條件;
  3. 通知全部等待在對象的線程
Synchronized(對象){
	業務邏輯處理,改變條件
	對象.notify/notifyAll
}
複製代碼

實例

public class User {
    private int age = 30;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    /**
     * 一、獲取對象鎖
     * 二、若是條件不知足,調用對象的wait方法,被通知後依然要檢查條件是否知足
     * 三、條件知足之後,才能執行相關的業務邏輯
     */
    public synchronized void waitAge(){
        System.out.println("age is " + this.age);
        while(this.age >= 20){
            //條件不知足
            try {
                System.out.println("current thread is waiting");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //知足條件後執行
        System.out.println("current thread" + Thread.currentThread().getName() + " age is " + this.age);
    }

    /**
     * 一、	得到對象的鎖;
     * 二、	改變條件;
     * 三、	通知全部等待在對象的線程
     */
    public synchronized void changeAge(){
        //修改條件
        this.age = new Random().nextInt(20);
        System.out.println("inform all thread");
        //這裏使用notifyAll()是由於notify()方法沒法指定喚醒某一個線程,notify()的喚醒是隨機的
        //notifyAll()喚醒全部等待線程
        notifyAll();
    }

}

複製代碼

測試類:

public class WaitAndInform extends Thread{

    private static User user = new User();

    @Override
    public void run() {
        user.waitAge();
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<=4;i++){
            new WaitAndInform().start();
        }
        Thread.sleep(1000);
        //修改條件 喚醒其餘線程
        user.changeAge();
    }
}
複製代碼
相關文章
相關標籤/搜索