1. 程式人生 > >淺談volatile 與 synchronized

淺談volatile 與 synchronized

一  volatile

1,不用volatile關鍵字

首先,讓我們瞭解一下JAVA的記憶體模型。

從圖中可以看出:

①每個執行緒都有一個自己的本地記憶體空間--執行緒棧空間。執行緒執行時,先把變數從主記憶體讀取到執行緒自己的本地記憶體空間,然後再對該變數進行操作

②對該變數操作完後,在某個時間再把變數重新整理回主記憶體

會發生的問題:可能造成一個執行緒在主記憶體中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致。

例:

public class RunThread extends Thread {

    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    @Override
    public void run() {
        System.out.println("進入到run方法中了");
        while (isRunning == true) {
        }
        System.out.println("執行緒執行完成了");
    }
}

 class Run {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

測試結果:執行緒始終沒有結束,一直堵塞在while迴圈裡面,而main執行緒已經改變了isRunnig變數,說明RunThread執行緒讀取不到 isRunnig 變數的變化。

2.用了volatile關鍵字

繼續使用上述的測試程式碼,給 isRunnig變數前面增加 volatile 關鍵字,發現測試結果正常。

這裡涉及到 volatile 關鍵字保證可見性的特性。

使用了volatile關鍵字的變數,會使處理器引發兩件事情:

A  將當前處理器快取的資料寫回系統記憶體

B  這個寫回記憶體的操作會導致其他處理器裡快取了該記憶體地址的資料無效。

簡而言之:

    沒有用volatile 之前是這樣的

加了volatile之後是這樣的,可以保證被volatile修飾的變數的每次改變,都會被其它執行緒看到,即可見性。

3.volatile關鍵字的其它特性

  volatile關鍵保證執行緒的有序性:為了提高執行效率,java中的編譯器和處理器可以對指令進行重新排序,重新排序會影響多執行緒併發的正確性,有序性就是要保證不進行重新排序。

例:

**

 * 一個簡單的展示Happen-Before的例子.

 * 這裡有兩個共享變數:a和flag,初始值分別為0和false.在ThreadA中先給a=1,然後flag=true.

 * 如果按照有序的話,那麼在ThreadB中如果if(flag)成功的話,則應該a=1,而a=a*1之後a仍然為1,下方的if(a==0)應該永遠不會為真,永遠不會列印.

 * 但實際情況是:在試驗100次的情況下會出現0次或幾次的列印結果,而試驗1000次結果更明顯,有十幾次列印.

 */

public class SimpleHappenBefore {

    /** 這是一個驗證結果的變數 */

    private static int a=0;

    /** 這是一個標誌位 */

    private static boolean flag=false;



    public static void main(String[] args) throws InterruptedException {

        //由於多執行緒情況下未必會試出重排序的結論,所以多試一些次

        for(int i=0;i<1000;i++){

            ThreadA threadA=new ThreadA();

            ThreadB threadB=new ThreadB();

            threadA.start();

            threadB.start();

            //這裡等待執行緒結束後,重置共享變數,以使驗證結果的工作變得簡單些.
            threadA.join();
            threadB.join();

            a=0;
            flag=false;

        }

    }



    static class ThreadA extends Thread{
        @Override
        public void run(){

            a=1;
            flag=true;

        }

    }



    static class ThreadB extends Thread{
        @Override
        public void run(){

            if(flag){
                a=a*1;
            }

            if(a==0){
                System.out.println("ha,a==0");
            }

        }

    }

}

測試結果:在試驗100次的情況下會出現0次或幾次的列印結果,而試驗1000次結果更明顯,有十幾次列印.並且在1000次的情況下,即使對兩個欄位添加了volatile,也會出現列印結果,猜想可能是由於B執行緒優先於A執行緒執行。

4.volatile關鍵字不保證原子性。

原子性:是指執行緒的多個操作是一個整體,不能被分割,要麼就不執行,要麼就全部執行完,中間不能被打斷。

比如,變數的自增操作 i++,分三個步驟:

①從記憶體中讀取出變數 i 的值

②將 i 的值加1

③將 加1 後的值寫回記憶體

這說明 i++ 並不是一個原子操作。因為,它分成了三步,有可能當某個執行緒執行到了第②時被中斷了,那麼就意味著只執行了其中的兩個步驟,沒有全部執行。

例:

class MyThread extends Thread {
    public volatile static int count;

    private static void addCount() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println("count=" + count);
    }

    @Override
    public void run() {
        addCount();
    }
}

  public class RunThread {
    public static void main(String[] args) {
        MyThread[] mythreadArray = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            mythreadArray[i] = new MyThread();
        }

        for (int i = 0; i < 100; i++) {
            mythreadArray[i].start();
        }
    }
}

期望的正確的結果應該是 100*100=10000,但是,實際上count並沒有達到10000

原因是:volatile修飾的變數並不保證對它的操作(自增)具有原子性。(對於自增操作,可以使用JAVA的原子類AutoicInteger類保證原子自增)

比如,假設 i 自增到 5,執行緒A從主記憶體中讀取i,值為5,將它儲存到自己的執行緒空間中,執行加1操作,值為6。此時,CPU切換到執行緒B執行,從主從記憶體中讀取變數i的值。 由於執行緒A還沒有來得及將加1後的結果寫回到主記憶體,執行緒B就已經從主記憶體中讀取了i,因此,執行緒B讀到的變數 i 值還是5

相當於執行緒B讀取的是已經過時的資料了,從而導致執行緒不安全性。

此處可能會有疑惑?這不是跟可見性違背了嗎?

樓主的理解是:線上程A把值為6這個結果更新到主記憶體中時,被打斷暫停,cpu執行執行緒B,而執行緒B從主記憶體中取出的還是5,導致最終結果小於10000.

 

二    synchronized

1.三種使用方式

A    修飾例項方法,作用於當前物件例項加鎖,進入同步程式碼前要獲得當前物件例項的鎖

B   修飾靜態方法,作用於當前類物件加鎖,進入同步程式碼前要獲得當前類物件的鎖 。

C  修飾程式碼塊,指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。

 

synchronized 關鍵字底層原理屬於 JVM 層面

1. synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令則指明同步程式碼塊的結束位置。 當執行 monitorenter 指令時,執行緒試圖獲取鎖也就是獲取 monitor(monitor物件存在於每個Java物件的物件頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因) 的持有權.當計數器為0則可以成功獲取,獲取後將鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設為0,表明鎖被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止。

2.synchronized 修飾的方法取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。

 

在 Java 早期版本中,synchronized 屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的 Mutex Lock 來實現的,Java 的執行緒是對映到作業系統的原生執行緒之上的。如果要掛起或者喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷

 

2.synchronized方法的缺點

使用synchronized關鍵字宣告方法有些時候是有很大的弊端的,比如我們有兩個執行緒一個執行緒A呼叫同步方法後獲得鎖,那麼另一個執行緒B獲取鎖就需要等待A執行完,但是如果說A執行的是一個很費時間的任務的話這樣就會很耗時。

通過synchronized同步語句塊解決這個問題

3.結論:

  1. 其他執行緒執行物件中synchronized同步方法和synchronized(this)程式碼塊時呈現同步效果(都是獲取當前物件的鎖);

  2.  如果兩個執行緒使用了同一個“物件監視器”,執行結果同步,否則不同步( synchronized (object) {……….}  );

  3.synchronized關鍵字加到static靜態方法和synchronized(xxx.class)程式碼塊上都是是給Class類上鎖,而synchronized關鍵字加到非static靜態方法上是給物件上鎖。

 4.儘量不要使用synchronized(string),而使用synchronized(object),因為String可能引用的是常量池同一物件,例  String s1 = "a";      String s2="a";

 

三  volatile 與 synchronized 對比

1 volatile輕量級,只能修飾變數。synchronized重量級,還可修飾方法。所以volatile的效能要好。

2 volatile只能保證資料的可見性,不保證資料的原子性,不能用來同步,因為多個執行緒併發訪問volatile修飾的變數不會阻塞。

  synchronize不僅保證可見性,而且還保證原子性,多個執行緒爭搶synchronized鎖物件時,會出現阻塞。

3.volatile關鍵字用於解決變數在多個執行緒之間的可見性,而ynchronized關鍵字解決的是多個執行緒之間訪問資源的同步性。

 

部分轉載至:https://www.cnblogs.com/hapjin/p/5492880.html