Java多執行緒之三volatile與等待通知機制示例

寧願。發表於2018-11-25

原子性,可見性與有序性

在多執行緒中,執行緒同步的時候一般需要考慮原子性,可見性與有序性

原子性

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

以我們在Java程式碼中經常用到的自增操作i++為例,i++實際上並不是一步操作,而是首先對i的值加一,然後將結果再賦值給i。在單執行緒中不會存在問題,但如果在多執行緒中我們考慮這樣一個情況:i是一個共享變數,初始值為0,假設執行緒一以執行到某一步正好進行自增操作i++,剛好對i進行了加一但是還沒將值重新賦給i,此時當前執行緒被cpu掛起,而另一個執行緒二開始執行,剛好也對i進行了一個賦值操作i=10;,等執行緒一重新執行後會將i自增後的值1賦給i,此時相當於覆蓋了執行緒二的賦值操作。此時將會產生執行緒不安全的情況。

可見性

多個執行緒同時訪問一個共享的變數的時候,每個執行緒的工作記憶體有這個變數的一個拷貝,變數本身還是儲存在共享記憶體(堆)中。所以並不是每一次一個執行緒修改了值後其他執行緒都可以立即取到修改後的值。可見性是指當其他的執行緒訪問同一個變數時,當一個執行緒修改了這個變數的值,其他執行緒也能夠立即看得到修改的值。

有序性

有序性是指程式的執行嚴格按照我們寫的程式碼的順序進行執行。

指令重排

一般情況下,CPU和編譯器為了提升程式執行的效率,會按照一定的規則允許對指令進行優化,即調整實際指令執行的順序。指令重排不會對單執行緒的程式造成任何不利的影響,但是多執行緒環境下將會產生一些影響。指令重排的前提條件是指令調整後不會影響單執行緒程式的執行:

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之後重新被重用的一個關鍵字,它可以保證上訴三大特性中的有序性和可見性,但是不能保證原子性,所以它實際上是執行緒不安全的。

保證可見性

出現可見性的原因在於私有棧幀中的值和公共堆中的共享值不同得問題。

私有棧和共享變數的資料同步

當一個執行緒在修改普通變數時,其他執行緒不能立刻看到修改後的值,如果此時有其他執行緒讀取該變數的值,實際上讀到的是沒有修改的值。

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;
    }

    /**
     * 1、獲取物件鎖
     * 2、如果條件不滿足,呼叫物件的wait方法,被通知後依然要檢查條件是否滿足
     * 3、條件滿足以後,才能執行相關的業務邏輯
     */
    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);
    }

    /**
     * 1、	獲得物件的鎖;
     * 2、	改變條件;
     * 3、	通知所有等待在物件的執行緒
     */
    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();
    }
}
複製程式碼

相關文章