天天看點

java中volatile、synchronized和lock解析1、概述2、volatile3、synchronized4、lock5、volatile和synchronized差別6、synchronized和lock差別

1、概述

在研究并發程式時,我們需要了解java中關鍵字volatile和synchronized關鍵字的使用以及lock類的用法。

首先,了解下java的記憶體模型:

java中volatile、synchronized和lock解析1、概述2、volatile3、synchronized4、lock5、volatile和synchronized差別6、synchronized和lock差別

(1)每個線程都有自己的本地記憶體空間(java棧中的幀)。線程執行時,先把變量從記憶體讀到線程自己的本地記憶體空間,然後對變量進行操作。

(2)對該變量操作完成後,在某個時間再把變量重新整理回主記憶體。

那麼我們再了解下鎖提供的兩種特性:互斥(mutual exclusion) 和可見性(visibility):

(1)互斥(mutual exclusion):互斥即一次隻允許一個線程持有某個特定的鎖,是以可使用該特性實作對共享資料的協調通路協定,這樣,一次就隻有一個線程能夠使用該共享資料;

(2)可見性(visibility):簡單來說就是一個線程修改了變量,其他線程可以立即知道。保證可見性的方法:volatile,synchronized,final(一旦初始化完成其他線程就可見)。

2、volatile

volatile是一個類型修飾符(type specifier)。它是被設計用來修飾被不同線程通路和修改的變量。確定本條指令不會因編譯器的優化而省略,且要求每次直接讀值。

上面的話有些拗口,簡單概括volatile,它能夠使變量在值發生改變時能盡快地讓其他線程知道。

(1)問題來源

首先我們要先意識到有這樣的現象,編譯器為了加快程式運作的速度,對一些變量的寫操作會先在寄存器或者是CPU緩存上進行,最後才寫入記憶體。而在這個過程中,變量的新值對其他線程是不可見的。

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("線程執行完成了");
    }
}

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

在main線程中,thread.setRunning(false);将啟動的線程RunThread中的共享變量設定為false,進而想讓RunThread.java的while循環結束。如果使用JVM -server參數執行該程式時,RunThread線程并不會終止,進而出現了死循環。

(2)原因分析

現在有兩個線程,一個是main線程,另一個是RunThread。它們都試圖修改isRunning變量。按照JVM記憶體模型,main線程将isRunning讀取到本地線程記憶體空間,修改後,再重新整理回主記憶體。

而在JVM設定成 -server模式運作程式時,線程會一直在私有堆棧中讀取isRunning變量。是以,RunThread線程無法讀到main線程改變的isRunning變量。進而出現了死循環,導緻RunThread無法終止。

(3)解決方法

volatile private boolean isRunning = true;
           

(4)原理

當對volatile标記的變量進行修改時,會将其他緩存中存儲的修改前的變量清除,然後重新讀取。一般來說應該是先在進行修改的緩存A中修改為新值,然後通知其他緩存清除掉此變量,當其他緩存B中的線程讀取此變量時,會向總線發送消息,這時存儲新值的緩存A擷取到消息,将新值穿給B。最後将新值寫入記憶體。當變量需要更新時都是此步驟,volatile的作用是被其修飾的變量,每次更新時,都會重新整理上述步驟。

3、synchronized

Java語言的關鍵字,可用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多隻有一個線程執行這段代碼。當兩個并發線程通路同一個對象object中的這個加鎖同步代碼塊時,一個時間内隻能有一個線程得到執行。另一個線程必須等待目前線程執行完這個代碼塊以後才能執行該代碼塊。然而,當一個線程通路object的一個加鎖代碼塊時,另一個線程仍然可以通路該object中的非加鎖代碼塊。

(1)synchronized 方法

方法聲明時使用,放在範圍操作符(public等)之後,傳回類型聲明(void等)之前.這時,線程獲得的是成員鎖,即一次隻能有一個線程進入該方法,其他線程要想在此時調用該方法,隻能排隊等候,目前線程(就是在synchronized方法内部的線程)執行完該方法後,别的線程才能進入。

示例:

public synchronized void synMethod(){
      //方法體
  }
           

如線上程t1中有語句obj.synMethod(); 那麼由于synMethod被synchronized修飾,在執行該語句前, 需要先獲得調用者obj的對象鎖, 如果其他線程(如t2)已經鎖定了obj (可能是通過obj.synMethod,也可能是通過其他被synchronized修飾的方法obj.otherSynMethod鎖定的obj), t1需要等待直到其他線程(t2)釋放obj, 然後t1鎖定obj, 執行synMethod方法. 傳回之前之前釋放obj鎖。

(2)synchronized 塊

對某一代碼塊使用,synchronized後跟括号,括号裡是變量,這樣,一次隻有一個線程進入該代碼塊.此時,線程獲得的是成員鎖。

(3)synchronized (this)

  1. 當兩個并發線程通路同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間内隻能有一個線程得到執行。另一個線程必須等待目前線程執行完這個代碼塊以後才能執行該代碼塊。

      

  2. 當一個線程通路object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的通路将被阻塞。  
  3. 然而,當一個線程通路object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以通路該object中的除synchronized(this)同步代碼塊以外的部分。 
  4. 第三個例子同樣适用其它同步代碼塊。也就是說,當一個線程通路object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的通路都被暫時阻塞。  
  5. 以上規則對其它對象鎖同樣适用。

第三點舉例說明:

public class Thread2 {  
     public void m4t1() {  
          synchronized(this) {  
               int i = ;  
               while( i-- > ) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep();  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }  
     }  
     public void m4t2() {  
          int i = ;  
          while( i-- > ) {  
               System.out.println(Thread.currentThread().getName() + " : " + i);  
               try {  
                    Thread.sleep();  
               } catch (InterruptedException ie) {  
               }  
          }  
     }  
     public static void main(String[] args) {  
          final Thread2 myt2 = new Thread2();  
          Thread t1 = new Thread(  new Runnable() {  public void run() {  myt2.m4t1();  }  }, "t1"  );  
          Thread t2 = new Thread(  new Runnable() {  public void run() { myt2.m4t2();   }  }, "t2"  );  
          t1.start();  
          t2.start();  
     } 
}
           

含有synchronized同步塊的方法m4t1被通路時,線程中m4t2()依然可以被通路。

(4)wait() 與notify()/notifyAll()

wait():釋放占有的對象鎖,線程進入等待池,釋放cpu,而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運作程式。而sleep()不同的是,線程調用此方法後,會休眠一段時間,休眠期間,會暫時釋放cpu,但并不釋放對象鎖。也就是說,在休眠期間,其他線程依然無法進入此代碼内部。休眠結束,線程重新獲得cpu,執行代碼。wait()和sleep()最大的不同在于wait()會釋放對象鎖,而sleep()不會!

notify(): 該方法會喚醒因為調用對象的wait()而等待的線程,其實就是對對象鎖的喚醒,進而使得wait()的線程可以有機會擷取對象鎖。調用notify()後,并不會立即釋放鎖,而是繼續執行目前代碼,直到synchronized中的代碼全部執行完畢,才會釋放對象鎖。JVM則會在等待的線程中排程一個線程去獲得對象鎖,執行代碼。需要注意的是,wait()和notify()必須在synchronized代碼塊中調用。

notifyAll()則是喚醒所有等待的線程。

4、lock

(1)synchronized的缺陷

synchronized是java中的一個關鍵字,也就是說是Java語言内置的特性。那麼為什麼會出現Lock呢?

如果一個代碼塊被synchronized修飾了,當一個線程擷取了對應的鎖,并執行該代碼塊時,其他線程便隻能一直等待,等待擷取鎖的線程釋放鎖,而這裡擷取鎖的線程釋放鎖隻會有兩種情況:

  1)擷取鎖的線程執行完了該代碼塊,然後線程釋放對鎖的占有;

  2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。

  那麼如果這個擷取鎖的線程由于要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便隻能等待,試想一下,這多麼影響程式執行效率。

  是以就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如隻等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。

再舉個例子:當有多個線程讀寫檔案時,讀操作和寫操作會發生沖突現象,寫操作和寫操作會發生沖突現象,但是讀操作和讀操作不會發生沖突現象。

  但是采用synchronized關鍵字來實作同步的話,就會導緻一個問題:

  如果多個線程都隻是進行讀操作,是以當一個線程在進行讀操作時,其他線程隻能等待無法進行讀操作。

  是以就需要一種機制來使得多個線程都隻是進行讀操作時,線程之間不會發生沖突,通過Lock就可以辦到。

  另外,通過Lock可以知道線程有沒有成功擷取到鎖。這個是synchronized無法辦到的。

  總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:

  1)Lock不是Java語言内置的,synchronized是Java語言的關鍵字,是以是内置特性。Lock是一個類,通過這個類可以實作同步通路;

  2)Lock和synchronized有一點非常大的不同,采用synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之後,系統會自動讓線程釋放對鎖的占用;而Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導緻出現死鎖現象。

(2)java.util.concurrent.locks包下常用的類

public interface Lock {
    //擷取鎖,如果鎖被其他線程擷取,則進行等待
    void lock(); 

    //當通過這個方法去擷取鎖時,如果線程正在等待擷取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀态。也就使說,當兩個線程同時通過lock.lockInterruptibly()想擷取某個鎖時,假若此時線程A擷取到了鎖,而線程B隻有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
    void lockInterruptibly() throws InterruptedException;

    /**tryLock()方法是有傳回值的,它表示用來嘗試擷取鎖,如果擷取成
    *功,則傳回true,如果擷取失敗(即鎖已被其他線程擷取),則傳回
    *false,也就說這個方法無論如何都會立即傳回。在拿不到鎖時不會一直在那等待。*/
    boolean tryLock();

    //tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,隻不過差別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之内如果還拿不到鎖,就傳回false。如果如果一開始拿到鎖或者在等待期間内拿到了鎖,則傳回true。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock(); //釋放鎖
    Condition newCondition();
}
           

通常使用lock進行同步:

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){

}finally{
    lock.unlock();   //釋放鎖
}
           

trylock使用方法:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){

     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能擷取鎖,則直接做其他事情
}
           

lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
           

注意:

當一個線程擷取了鎖之後,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運作過程中的線程,隻能中斷阻塞過程中的線程。

而用synchronized修飾的話,當一個線程處于等待某個鎖的狀态,是無法被中斷的,隻有一直等待下去。

(3)ReentrantLock

ReentrantLock,意思是“可重入鎖”,是唯一實作了Lock接口的類,并且ReentrantLock提供了更多的方法。

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個地方
    public static void main(String[] args)  {
        final Test test = new Test();

        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  

    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=;i<;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}
           

如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了鎖的配置設定機制:基于線程的配置設定,而不是基于方法調用的配置設定。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。

代碼解釋:

class MyClass {
    public synchronized void method1() {
        method2();
    }

    public synchronized void method2() {

    }
}
           

上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A擷取了這個對象的鎖,而由于method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因為線程A已經持有了該對象的鎖,而又在申請擷取該對象的鎖,這樣就會線程A一直等待永遠不會擷取到的鎖。

  而由于synchronized和Lock都具備可重入性,是以不會發生上述現象。

5、volatile和synchronized差別

1)volatile本質是在告訴jvm目前變量在寄存器中的值是不确定的,需要從主存中讀取,synchronized則是鎖定目前變量,隻有目前線程可以通路該變量,其他線程被阻塞住.

2)volatile僅能使用在變量級别,synchronized則可以使用在變量,方法.

3)volatile僅能實作變量的修改可見性,而synchronized則可以保證變量的修改可見性和原子性.

  《Java程式設計思想》上說,定義long或double變量時,如果使用volatile關鍵字,就會獲得(簡單的指派與傳回操作)原子性。

  

4)volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞.

5、當一個域的值依賴于它之前的值時,volatile就無法工作了,如n=n+1,n++等。如果某個域的值受到其他域的值的限制,那麼volatile也無法工作,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。

6、使用volatile而不是synchronized的唯一安全的情況是類中隻有一個可變的域。

6、synchronized和lock差別

  1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是内置的語言實作;

  2)synchronized在發生異常時,會自動釋放線程占有的鎖,是以不會導緻死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,是以使用Lock時需要在finally塊中釋放鎖;

  3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;

  4)通過Lock可以知道有沒有成功擷取鎖,而synchronized卻無法辦到。

  5)Lock可以提高多個線程進行讀操作的效率。

  在性能上來說,如果競争資源不激烈,兩者的性能是差不多的,而當競争資源非常激烈時(即有大量線程同時競争),此時Lock的性能要遠遠優于synchronized。是以說,在具體使用時要根據适當情況選擇。