1. 程式人生 > >Java多線程之三volatile與等待通知機制示例

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

不存在 跳出循環 三種 安全 同步 完成後 了解 try code

原子性,可見性與有序性

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

原子性

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

以我們在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();
    }
}

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