Java中volatile、synchronized和lock解析

1、概述

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


首先,瞭解下java的內存模型:

(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(1000);
            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)

當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。 

  

當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。  


然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的除synchronized(this)同步代碼塊以外的部分。 


第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。  


以上規則對其它對象鎖同樣適用。

第三點舉例說明:

public class Thread2 {  
     public void m4t1({  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }  
     }  
     public void m4t2({  
          int i = 5;  
          while( i-- > 0) {  
               System.out.println(Thread.currentThread().getName() + " : " + i);  
               try {  
                    Thread.sleep(500);  
               } 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=0;i<5;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。所以說,在具體使用時要根據適當情況選擇。

來源:java一日一條

微信圖片_20171210074204.jpg

公衆號:IT哈哈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章