Java中volatile、synchronized和lock解析

一、概述

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


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

(1)每一個線程都有本身的本地內存空間(java棧中的幀)。線程執行時,先把變量從內存讀到線程本身的本地內存空間,而後對變量進行操做。 編程

(2)對該變量操做完成後,在某個時間再把變量刷新回主內存。緩存


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


(1)互斥(mutual exclusion):互斥即一次只容許一個線程持有某個特定的鎖,所以可以使用該特性實現對共享數據的協調訪問協議,這樣,一次就只有一個線程可以使用該共享數據;微信


(2)可見性(visibility):簡單來講就是一個線程修改了變量,其餘線程能夠當即知道。保證可見性的方法:volatile,synchronized,final(一旦初始化完成其餘線程就可見)。併發

二、volatile

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


上面的話有些拗口,簡單歸納volatile,它可以使變量在值發生改變時能儘快地讓其餘線程知道。jvm


(1)問題來源ide

首先咱們要先意識到有這樣的現象,編譯器爲了加快程序運行的速度,對一些變量的寫操做會先在寄存器或者是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的做用是被其修飾的變量,每次更新時,都會刷新上述步驟。


三、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()則是喚醒全部等待的線程。


四、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都具有可重入性,因此不會發生上述現象。


五、volatile和synchronized區別

1)volatile本質是在告訴jvm當前變量在寄存器中的值是不肯定的,須要從主存中讀取,synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住.


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


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


  《Java編程思想》上說,定義long或double變量時,若是使用volatile關鍵字,就會得到(簡單的賦值與返回操做)原子性。 

   

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


五、當一個域的值依賴於它以前的值時,volatile就沒法工做了,如n=n+1,n++等。若是某個域的值受到其餘域的值的限制,那麼volatile也沒法工做,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。


六、使用volatile而不是synchronized的惟一安全的狀況是類中只有一個可變的域。


六、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哈哈

相關文章
相關標籤/搜索