Synchronized與Lock的區別與應用場景


 

轉載。 https://blog.csdn.net/fly910905/article/details/79765381

同步代碼塊,同步方法,或者是用java提供的鎖機制,我們可以實現對共享資源變量的同步控制。

 

技術點:

1、線程與進程:

在開始之前先把進程與線程進行區分一下,一個程序最少需要一個進程,而一個進程最少需要一個線程。關系是線程–>進程–>程序的大致組成結構。所以線程是程序執行流的最小單位,而進程是系統進行資源分配和調度的一個獨立單位。以下我們所有討論的都是建立在線程基礎之上。

2、Thread的幾個重要方法:

我們先了解一下Thread的幾個重要方法。

 

  • a、start()方法,開始執行該線程;
  • b、stop()方法,強制結束該線程執行;
  • c、join方法,等待該線程結束。
  • d、sleep()方法,線程進入等待。
  • e、run()方法,直接執行線程的run()方法,但是線程調用start()方法時也會運行run()方法,區別就是一個是由線程調度運行run()方法,一個是直接調用了線程中的run()方法!!

看到這里,可能有些人就會問啦,那wait()和notify()呢?要注意,其實wait()與notify()方法是Object的方法,不是Thread的方法!!同時,wait()與notify()會配合使用,分別表示線程掛起和線程恢復。

這里還有一個很常見的問題,順帶提一下:wait()與sleep()的區別,簡單來說wait()會釋放對象鎖而sleep()不會釋放對象鎖。

3、線程狀態:

這里寫圖片描述

線程總共有5大狀態,通過上面第二個知識點的介紹,理解起來就簡單了。

  • 新建狀態:新建線程對象,並沒有調用start()方法之前

  • 就緒狀態:調用start()方法之后線程就進入就緒狀態,但是並不是說只要調用start()方法線程就馬上變為當前線程,在變為當前線程之前都是為就緒狀態。值得一提的是,線程在睡眠和掛起中恢復的時候也會進入就緒狀態哦。

  • 運行狀態:線程被設置為當前線程,開始執行run()方法。就是線程進入運行狀態

  • 阻塞狀態:線程被暫停,比如說調用sleep()方法后線程就進入阻塞狀態

  • 死亡狀態:線程執行結束

4、鎖類型

  • 可重入鎖(synchronized和ReentrantLock):在執行對象中所有同步方法不用再次獲得鎖

  • 可中斷鎖(synchronized就不是可中斷鎖,而Lock是可中斷鎖):在等待獲取鎖過程中可中斷

  • 公平鎖(ReentrantLock和ReentrantReadWriteLock): 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具有優先獲取鎖權利

  • 讀寫鎖(ReadWriteLock和ReentrantReadWriteLock):對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多線程一起讀,寫的時候必須同步地寫

Synchronized與Lock的區別

類別 synchronized Lock
存在層次 Java的關鍵字,在jvm層面上 是一個接口
鎖的釋放

1、以獲取鎖的線程執行完同步代碼,釋放鎖 

2、線程執行發生異常,jvm會讓線程釋放鎖

在finally中必須釋放鎖,不然容易造成線程死鎖
鎖的獲取

假設A線程獲得鎖,B線程等待。

如果A線程阻塞,B線程會一直等待

分情況而定,Lock有多個鎖獲取的方式,大致就是可以嘗試獲得鎖,線程可以不用一直等待(可以通過tryLock判斷有沒有鎖)
鎖狀態 無法判斷 可以判斷
鎖類型 可重入 不可中斷 非公平 可重入 可判斷 可公平(兩者皆可)
性能 少量同步

大量同步

 

  • Lock可以提高多個線程進行讀操作的效率。(可以通過readwritelock實現讀寫分離)
  • 在資源競爭不是很激烈的情況下,Synchronized的性能要優於ReetrantLock,但是在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態;
  • ReentrantLock提供了多樣化的同步,比如有時間限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在資源競爭不激烈的情形下,性能稍微比synchronized差點點。但是當同步非常激烈的時候,synchronized的性能一下子能下降好幾十倍。而ReentrantLock確還能維持常態。

 


Synchronized與Static Synchronized

 

每個類有一個鎖,它可以用來控制對static數據成員的並發訪問。 訪問static synchronized方法占用的是類鎖,而訪問非static synchronized方法占用的是對象鎖。 static synchronized控制類的所有實例(對象)的訪問(相應代碼塊)。 synchronized相當於 this.synchronized,static synchronized相當於Something.synchronized

Lock接口

Lock是一個接口

  1.  
    public interface Lock {
  2.  
    void lock();
  3.  
    void lockInterruptibly() throws InterruptedException;
  4.  
    boolean tryLock();
  5.  
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  6.  
    void unlock();
  7.  
    Condition newCondition();
  8.  
    }
  • lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。
  • unLock()方法是用來釋放鎖的。

在Lock中聲明了四個方法來獲取鎖,那么這四個方法有何區別呢?

  首先lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。

  由於在前面講到如果采用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:

  1.  
    Lock lock = ...;
  2.  
    lock.lock();
  3.  
    try{
  4.  
    //處理任務
  5.  
    } catch(Exception ex){
  6.  
     
  7.  
    } finally{
  8.  
    lock.unlock(); //釋放鎖
  9.  
    }
 

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

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  所以,一般情況下通過tryLock來獲取鎖時是這樣使用的:

  1.  
    Lock lock = ...;
  2.  
    if(lock.tryLock()) {
  3.  
    try{
  4.  
    //處理任務
  5.  
    } catch(Exception ex){
  6.  
     
  7.  
    } finally{
  8.  
    lock.unlock(); //釋放鎖
  9.  
    }
  10.  
    } else {
  11.  
    //如果不能獲取鎖,則直接做其他事情
  12.  
    }
 

   lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。

  由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。

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

  1.  
    public void method() throws InterruptedException {
  2.  
    lock.lockInterruptibly();
  3.  
    try {
  4.  
    //.....
  5.  
    }
  6.  
    finally {
  7.  
    lock.unlock();
  8.  
    }
  9.  
    }
 

  注意,當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。

  因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。

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

 

 

Lock類型

 

 

一、公平鎖/非公平鎖

 

  • 公平鎖是指多個線程按照申請鎖的順序來獲取鎖。
  • 非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者飢餓現象。
  • 對於ReentrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。
  • 對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的來實現線程調度,所以並沒有任何辦法使其變成公平鎖。

二、可重入鎖

 

  • 可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。
  • 說的有點抽象,下面會有一個代碼的示例。
  • 對於Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖,其名字是Re entrant Lock重新進入鎖。
  • 對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。
  1.  
    synchronized void setA() throws Exception{
  2.  
     
  3.  
        Thread.sleep( 1000);
  4.  
     
  5.  
        setB();
  6.  
     
  7.  
    }
  8.  
     
  9.  
    synchronized void setB() throws Exception{
  10.  
     
  11.  
        Thread.sleep( 1000);
  12.  
     
  13.  
    }

三、獨享鎖/共享鎖

  • 獨享鎖是指該鎖一次只能被一個線程所持有。
  • 共享鎖是指該鎖可被多個線程所持有。
  • 對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
  • 讀鎖的共享鎖可保證並發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
  • 獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。
  • 對於Synchronized而言,當然是獨享鎖。

四、互斥鎖/讀寫鎖

  • 上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。
  • 互斥鎖在Java中的具體實現就是ReentrantLock
  • 讀寫鎖在Java中的具體實現就是ReadWriteLock

五、樂觀鎖/悲觀鎖

  • 樂觀鎖與悲觀鎖不是指具體的什么類型的鎖,而是指看待並發同步的角度。
  • 悲觀鎖認為對於同一個數據的並發操作,一定是會發生修改的,哪怕沒有修改,也會認為修改。因此對於同一個數據的並發操作,悲觀鎖采取加鎖的形式。悲觀的認為,不加鎖的並發操作一定會出問題。
  • 樂觀鎖則認為對於同一個數據的並發操作,是不會發生修改的。在更新數據的時候,會采用嘗試更新,不斷重新的方式更新數據。樂觀的認為,不加鎖的並發操作是沒有事情的。
  • 從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。
  • 悲觀鎖在Java中的使用,就是利用各種鎖。
  • 樂觀鎖在Java中的使用,是無鎖編程,常常采用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。

六、分段鎖

  • 分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其並發的實現就是通過分段鎖的形式來實現高效的並發操作。
  • 我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
  • 當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然后對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
  • 但是,在統計size的時候,可就是獲取hashmap全局信息的時候,就需要獲取所有的分段鎖才能統計。
  • 分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

 

七、偏向鎖/輕量級鎖/重量級鎖

 

  • 這三種鎖是指鎖的狀態,並且是針對Synchronized。在Java 5通過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的。
  • 偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖。降低獲取鎖的代價。
  • 輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
  • 重量級鎖是指當鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。

八、自旋鎖

 

  • 在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。
線程自旋和適應性自旋 

我們知道,java線程其實是映射在內核之上的,線程的掛起和恢復會極大的影響開銷。
並且jdk官方人員發現,很多線程在等待鎖的時候,在很短的一段時間就獲得了鎖,所以它們在線程等待的時候,並不需要把線程掛起,而是讓他無目的的循環,一般設置10次。
這樣就避免了線程切換的開銷,極大的提升了性能。 

而適應性自旋,是賦予了自旋一種學習能力,它並不固定自旋10次一下。
他可以根據它前面線程的自旋情況,從而調整它的自旋,甚至是不經過自旋而直接掛起。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM