《面試補習》- Java鎖知識大梳理

面試補習系列:java

1、鎖的分類

一、樂觀鎖和悲觀鎖

樂觀鎖就是樂觀的認爲不會發生衝突,用cas和版本號實現 悲觀鎖就是認爲必定會發生衝突,對操做上鎖node

1.悲觀鎖

悲觀鎖,老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。面試

傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。
再好比 Java 裏面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。
複製代碼

適用場景:數據庫

比較適合寫入操做比較頻繁的場景,若是出現大量的讀取操做,每次讀取的時候都會進行加鎖,這樣會增長大量的鎖的開銷,下降了系統的吞吐量。數組

實現方式: synchronizedLock安全

2.樂觀鎖

每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制bash

ABA問題(JDK1.5以後已有解決方案):CAS須要在操做值的時候檢查內存值是否發生變化,沒有發生變化纔會更新內存值。可是若是內存值原來是A,後來變成了B,而後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,可是其實是有變化的。ABA問題的解決思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從「A-B-A」變成了「1A-2B-3A」。

循環時間長開銷大:CAS操做若是長時間不成功,會致使其一直自旋,給CPU帶來很是大的開銷。

只能保證一個共享變量的原子操做(JDK1.5以後已有解決方案):對一個共享變量執行操做時,CAS可以保證原子操做,可是對多個共享變量操做時,CAS是沒法保證操做的原子性的。
複製代碼

適用場景:markdown

比較適合讀取操做比較頻繁的場景,若是出現大量的寫入操做,數據發生衝突的可能性就會增大,爲了保證數據的一致性,應用層須要不斷的從新獲取數據,這樣會增長大量的查詢操做,下降了系統的吞吐量。數據結構

實現方式:多線程

一、使用版本標識來肯定讀到的數據與提交時的數據是否一致。提交後修改版本標識,不一致時能夠採起丟棄和再次嘗試的策略。

二、Java 中的 Compare and Swap 即 CAS ,當多個線程嘗試使用 CAS 同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。

三、在 Java 中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀鎖的一種實現方式 CAS 實現的。

二、公平鎖/非公平鎖

公平鎖:

指多個線程按照申請鎖的順序來獲取鎖。
複製代碼

非公平鎖:

指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。
有可能,會形成優先級反轉或者飢餓現象。

複製代碼

拓展線程飢餓:

一個或者多個線程由於種種緣由沒法得到所須要的資源,致使一直沒法執行的狀態
致使沒法獲取的緣由:
線程優先級較低,沒辦法獲取cpu時間
其餘線程老是能在它以前持續地對該同步塊進行訪問。
線程在等待一個自己也處於永久等待完成的對象(好比調用這個對象的 wait 方法),由於其餘線程老是被持續地得到喚醒。

複製代碼

實現方式: ReenTrantLock(公平/非公平)

對於Java ReentrantLock而言,經過構造函數指定該鎖是不是公平鎖,默認是非公平鎖。非公平鎖的優勢在於吞吐量比公平鎖大。

對於Synchronized而言,也是一種非公平鎖。因爲其並不像ReentrantLock是經過AQS(AbstractQueuedSynchronizer)的來實現線程調度,因此並無任何辦法使其變成公平鎖。

三、可重入鎖

若是一個線程得到過該鎖,能夠再次得到,主要是用途就是在遞歸方面,還有就是防止死鎖,好比在一個同步方法塊中調用了另外一個相同鎖對象的同步方法塊

實現方式: synchronizedReentrantLock

四、獨享鎖/共享鎖

獨享鎖是指該鎖一次只能被一個線程所持有。
共享鎖是指該鎖可被多個線程所持有。
複製代碼

實現方式: 獨享鎖: ReentrantLocksynchronized 貢獻鎖: ReadWriteLock

拓展:

互斥鎖/讀寫鎖 就是對上面的一種具體實現:

互斥鎖:在Java中的具體實現就是ReentrantLock,synchronized
讀寫鎖:在Java中的具體實現就是ReadWriteLock
複製代碼

對於Java ReentrantLock而言,其是獨享鎖。可是對於Lock的另外一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是很是高效的,讀寫,寫讀 ,寫寫的過程是互斥的。獨享鎖與共享鎖也是經過AQS來實現的,經過實現不一樣的方法,來實現獨享或者共享。對於Synchronized而言,固然是獨享鎖

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

基於 jdk 1.6 以上

偏向鎖指的是當前只有這個線程得到,沒有發生爭搶,此時將方法頭的markword設置成0,而後每次過來都cas一下就好,不用重複的獲取鎖.指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價

輕量級鎖:在偏向鎖的基礎上,有線程來爭搶,此時膨脹爲輕量級鎖,多個線程獲取鎖時用cas自旋獲取,而不是阻塞狀態

重量級鎖:輕量級鎖自旋必定次數後,膨脹爲重量級鎖,其餘線程阻塞,當獲取鎖線程釋放鎖後喚醒其餘線程。(線程阻塞和喚醒比上下文切換的時間影響大的多,涉及到用戶態和內核態的切換)

實現方式: synchronized

六、分段鎖

在1.7的concurrenthashmap中有分段鎖的實現,具體爲默認16個的segement數組,其中segement繼承自reentranklock,每一個線程過來獲取一個鎖,而後操做這個鎖下連着的map。

實現方式:

咱們以ConcurrentHashMap來講一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,
它即相似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表;
同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

當須要put元素的時候,並非對整個hashmap進行加鎖,而是先經過hashcode來知道他要放在那一個分段中,
而後對這個分段進行加鎖,因此當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。

可是,在統計size的時候,可就是獲取hashmap全局信息的時候,就須要獲取全部的分段鎖才能統計。

分段鎖的設計目的是細化鎖的粒度,當操做不須要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操做。
複製代碼

2、鎖的底層實現

一、Synchronized

synchronized 關鍵字經過一對字節碼指令 monitorenter/monitorexit 實現

前置知識:

對象頭:
Hotspot 虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。其中:

Klass Point 是是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。

Mark Word 用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵,因此下面將重點闡述 Mark Word 。

Monitor:
每個 Java 對象都有成爲Monitor 的潛質,由於在 Java 的設計中 ,每個 Java 對象自打孃胎裏出來就帶了一把看不見的鎖,它叫作內部鎖或者 Monitor 鎖

複製代碼

對象頭結構:

Monitor數據結構:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
參考: https://blog.csdn.net/javazejian/article/details/72828483
複製代碼

ObjectMonitor中有兩個隊列,_WaitSet_EntryList,用來保存ObjectWaiter對象列表( 每一個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1.

若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其餘線程進入獲取monitor(鎖)

這裏比較複雜,可是建議仔細閱讀,便於後續分析的時候理解
複製代碼

1.一、字節碼實現

同步代碼塊:
public class SynchronizedTest {

    public void test2() {
        synchronized(this) {
        }
    }
}
複製代碼

synchronized關鍵字基於上述兩個指令實現了鎖的獲取和釋放過程:

monitorenter指令插入到同步代碼塊的開始位置,

monitorexit 指令插入到同步代碼塊的結束位置.

線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 Monitor 全部權,即嘗試獲取對象的鎖。

當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程能夠成功取得 monitor,並將計數器值設置爲 1,取鎖成功。若是當前線程已經擁有 objectref 的 monitor 的持有權,那它能夠重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。假若其餘線程已經擁有 objectref 的 monitor 的全部權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其餘線程將有機會持有 monitor 。

複製代碼
同步方法:
synchronized 方法則會被翻譯成普通的方法調用和返回指令如:
invokevirtual、areturn 指令,在 JVM 字節碼層面並無任何特別的指令來實現被synchronized 修飾的方法,
而是在 Class 文件的方法表中將該方法的 access_flags 字段中的 synchronized 標誌位置設置爲 1,
表示該方法是同步方法,並使用調用該方法的對象或該方法所屬的 Class 
在 JVM 的內部對象表示 Klass 做爲鎖對象
複製代碼
//省略不必的字節碼
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法標識ACC_PUBLIC表明public修飾,ACC_SYNCHRONIZED指明該方法爲同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2 // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2 // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"
複製代碼

如下部分參考: JVM源碼分析之synchronized實現

1.二、偏向鎖獲取

一、獲取對象頭的Mark Word;
二、判斷mark是否爲可偏向狀態,即mark的偏向鎖標誌位爲 1,鎖標誌位爲 01;
三、判斷mark中JavaThread的狀態:若是爲空,則進入步驟(4);若是指向當前線程,
則執行同步代碼塊;若是指向其它線程,進入步驟(5);
四、經過CAS原子指令設置mark中JavaThread爲當前線程ID,
若是執行CAS成功,則執行同步代碼塊,不然進入步驟(5);
五、若是執行CAS失敗,表示當前存在多個線程競爭鎖,當達到全局安全點(safepoint),
得到偏向鎖的線程被掛起,撤銷偏向鎖,並升級爲輕量級,升級完成後被阻塞在安全點的線程繼續執行同步代碼塊;
複製代碼

在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,所以爲了減小同一線程獲取鎖(會涉及到一些CAS操做,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操做,從而也就提供程序的性能。因此,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續屢次是同一個線程申請相同的鎖。

注意 JVM 提供了關閉偏向鎖的機制, JVM 啓動命令指定以下參數便可

-XX:-UseBiasedLocking
複製代碼

偏向鎖的撤銷:

偏向鎖的 撤銷(revoke) 是一個很特殊的操做, 爲了執行撤銷操做, 須要等待全局安全點(Safe Point), 
此時間點全部的工做線程都中止了字節碼的執行。

偏向鎖這個機制很特殊, 別的鎖在執行完同步代碼塊後, 都會有釋放鎖的操做, 而偏向鎖並無直觀意義上的「釋放鎖」操做。

引入一個概念 epoch, 其本質是一個時間戳 , 表明了偏向鎖的有效性
複製代碼

1.三、輕量級鎖

在多線程交替執行同步塊的狀況下,儘可能避免重量級鎖引發的性能消耗,可是若是多個線程在同一時刻進入臨界區,會致使輕量級鎖膨脹升級重量級鎖,因此輕量級鎖的出現並不是是要替代重量級鎖。

一、獲取對象的markOop數據mark;
二、判斷mark是否爲無鎖狀態:mark的偏向鎖標誌位爲 0,鎖標誌位爲 01;
三、若是mark處於無鎖狀態,則進入步驟(4),不然執行步驟(6);
四、把mark保存到BasicLock對象的_displaced_header字段;
五、經過CAS嘗試將Mark Word更新爲指向BasicLock對象的指針,若是更新成功,表示競爭到鎖,則執行同步代碼,不然執行步驟(6);
六、若是當前mark處於加鎖狀態,且mark中的ptr指針指向當前線程的棧幀,則執行同步代碼,不然說明有多個線程競爭輕量級鎖,輕量級鎖須要膨脹升級爲重量級鎖;

複製代碼

1.四、重量級鎖

重量級鎖經過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操做系統的Mutex Lock實現,操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。

鎖膨脹過程:

一、整個膨脹過程在自旋下完成;
二、mark->has_monitor()方法判斷當前是否爲重量級鎖,即Mark Word的鎖標識位爲 10,若是當前狀態爲重量級鎖,執行步驟(3),不然執行步驟(4);
三、mark->monitor()方法獲取指向ObjectMonitor的指針,並返回,說明膨脹過程已經完成;
四、若是當前鎖處於膨脹中,說明該鎖正在被其它線程執行膨脹操做,則當前線程就進行自旋等待鎖膨脹完成,這裏須要注意一點,
雖然是自旋操做,但不會一直佔用cpu資源,每隔一段時間會經過os::NakedYield方法放棄cpu資源,或經過park方法掛起;
若是其餘線程完成鎖的膨脹操做,則退出自旋並返回;
五、若是當前是輕量級鎖狀態,即鎖標識位爲 00

複製代碼

Monitor 競爭:

一、經過CAS嘗試把monitor的_owner字段設置爲當前線程;
二、若是設置以前的_owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執行_recursions ++ ,記錄重入的次數;
三、若是以前的_owner指向的地址在當前線程中,這種描述有點拗口,換一種說法:以前_owner指向的BasicLock在當前線程棧上,
說明當前線程是第一次進入該monitor,設置_recursions爲1,_owner爲當前線程,該線程成功得到鎖並返回;
四、若是獲取鎖失敗,則等待鎖的釋放;

複製代碼

其本質就是經過CAS設置monitor的_owner字段爲當前線程,若是CAS成功,則表示該線程獲取了鎖,跳出自旋操做,執行同步代碼,不然繼續被掛起;

Monitor 釋放:

當某個持有鎖的線程執行完同步代碼塊時,會進行鎖的釋放,給其它線程機會執行同步代碼,在HotSpot中,經過退出monitor的方式實現鎖的釋放,並通知被阻塞的線程.

1.五、鎖優化內容

鎖消除:

消除鎖是虛擬機另一種鎖的優化,這種優化更完全,
Java虛擬機在JIT編譯時(能夠簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),
經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,
經過這種方式消除沒有必要的鎖,能夠節省毫無心義的請求鎖時間
複製代碼

鎖粗化:

將多個連續的加鎖、解鎖操做鏈接在一塊兒,擴展成一個範圍更大的鎖。
複製代碼

自旋鎖:

線程的阻塞和喚醒,須要 CPU 從用戶態轉爲核心態。頻繁的阻塞和喚醒對 CPU 來講是一件負擔很重的工做,勢必會給系統的併發性能帶來很大的壓力。
同時,咱們發如今許多應用上面,對象鎖的鎖狀態只會持續很短一段時間。爲了這一段很短的時間,頻繁地阻塞和喚醒線程是很是不值得的

適應性自旋鎖:
自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定
複製代碼

鎖升級:

二、ReetrantLock

2.一、Lock

//加鎖
    void lock();

    //解鎖
    void unlock();

    //可中斷獲取鎖,與lock()不一樣之處在於可響應中斷操做,即在獲
    //取鎖的過程當中可中斷,注意synchronized在獲取鎖時是不可中斷的
    void lockInterruptibly() throws InterruptedException;

    //嘗試非阻塞獲取鎖,調用該方法後當即返回結果,若是可以獲取則返回true,不然返回false
    boolean tryLock();

    //根據傳入的時間段獲取鎖,在指定時間內沒有獲取鎖則返回false,若是在指定時間內當前線程未被中並斷獲取到鎖則返回true
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //獲取等待通知組件,該組件與當前鎖綁定,當前線程只有得到了鎖
    //才能調用該組件的wait()方法,而調用後,當前線程將釋放鎖。
    Condition newCondition();

複製代碼

在Java 1.5中,官方在concurrent併發包(J.U.C)中加入了Lock接口,該接口中提供了lock()方法和unLock()方法對顯式加鎖和顯式釋放鎖操做進行支持.

Lock 鎖提供的優點:

可使鎖更公平。
可使線程在等待鎖的時候響應中斷。
可讓線程嘗試獲取鎖,並在沒法獲取鎖的時候當即返回或者等待一段時間。
能夠在不一樣的範圍,以不一樣的順序獲取和釋放鎖。
複製代碼

2.二、AQS (AbstractQueuedSynchronizer)

AQS 即隊列同步器。它是構建鎖或者其餘同步組件的基礎框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),J.U.C 併發包的做者(Doug Lea)指望它可以成爲實現大部分同步需求的基礎。

數據結構:

//同步隊列頭節點
    private transient volatile Node head;

    //同步隊列尾節點
    private transient volatile Node tail;

    //同步狀態
    private volatile int state;
複製代碼

AQS 使用一個 int 類型的成員變量 state 來表示同步狀態:

  • state > 0 時,表示已經獲取了鎖。
  • state = 0 時,表示釋放了鎖。

Node構成FIFO的同步隊列來完成線程獲取鎖的排隊工做

  • 若是當前線程獲取同步狀態失敗(鎖)時,AQS 則會將當前線程以及等待狀態等信息構形成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程
  • 當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態

參考: 深刻剖析基於併發AQS的(獨佔鎖)重入鎖(ReetrantLock)及其Condition實現原理

2.三、Sync

Sync:抽象類,是ReentrantLock的內部類,繼承自AbstractQueuedSynchronizer,實現了釋放鎖的操做(tryRelease()方法),並提供了lock抽象方法,由其子類實現。

NonfairSync:是ReentrantLock的內部類,繼承自Sync,非公平鎖的實現類。

FairSync:是ReentrantLock的內部類,繼承自Sync,公平鎖的實現類。

AQS、Sync 和 ReentrantLock 的具體關係圖:

2.四、ReentrantLock 實現原理

構造函數:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製代碼

ReentrantLock 提供兩種實現方式,公平鎖/非公平鎖. 經過構造函數進行初始化 sync 進行判斷當前鎖得類型.

2.4.一、非公平鎖(NonfairSync)
final void lock() {
    //cas 獲取鎖
        if (compareAndSetState(0, 1))
        //若是成功設置當前線程Id
            setExclusiveOwnerThread(Thread.currentThread());
        else
        //不然再次請求同步狀態
            acquire(1);
    }
複製代碼

先對同步狀態執行CAS操做,嘗試把state的狀態從0設置爲1, 若是返回true則表明獲取同步狀態成功,也就是當前線程獲取鎖成,可操做臨界資源,若是返回false,則表示已有線程持有該同步狀態(其值爲1) 獲取鎖失敗,注意這裏存在併發的情景,也就是可能同時存在多個線程設置state變量,所以是CAS操做保證了state變量操做的原子性。返回false後,執行acquire(1)方法

#acquire(int arg)方法,爲 AQS 提供的模板方法。該方法爲獨佔式獲取同步狀態,可是該方法對中斷不敏感。也就是說,因爲線程獲取同步狀態失敗而加入到 CLH 同步隊列中,後續對該線程進行中斷操做時,線程不會從 CLH 同步隊列中移除。

acquire 代碼:

public final void acquire(int arg) {
   //嘗試獲取同步狀態
       if (!tryAcquire(arg) &&
           //自旋直到得到同步狀態成功,添加節點到隊列    
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }
複製代碼

一、tryAcquire 嘗試獲取同步狀態

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //鎖閒置
            if (c == 0) {
            //CAS佔用
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //若是鎖state=1 && 線程爲當前線程 重入鎖的邏輯
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
複製代碼

二、acquireQueued 加入隊列中,自旋獲取鎖

private Node addWaiter(Node mode) {
   //將請求同步狀態失敗的線程封裝成結點
   Node node = new Node(Thread.currentThread(), mode);

   Node pred = tail;
   //若是是第一個結點加入確定爲空,跳過。
   //若是非第一個結點則直接執行CAS入隊操做,嘗試在尾部快速添加
   if (pred != null) {
       node.prev = pred;
       //使用CAS執行尾部結點替換,嘗試在尾部快速添加
       if (compareAndSetTail(pred, node)) {
           pred.next = node;
           return node;
       }
   }
   //若是第一次加入或者CAS操做沒有成功執行enq入隊操做
   enq(node);
   return node;
}

   final boolean acquireQueued(final Node node, int arg) {
       boolean failed = true;
       try {
           boolean interrupted = false;
           for (;;) {
           //獲取前驅節點
               final Node p = node.predecessor();
               //若是前驅節點試頭節點, 嘗試獲取同步狀態
               if (p == head && tryAcquire(arg)) {
                   setHead(node);
                   p.next = null; // help GC
                   failed = false;
                   return interrupted;
               }
               // 獲取失敗,線程等待
               if (shouldParkAfterFailedAcquire(p, node) &&
                   parkAndCheckInterrupt())
                   interrupted = true;
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
   }

複製代碼

流程圖:

2.4.二、公平鎖(FairSync)

與非公平鎖不一樣的是,在獲取鎖的時,公平鎖的獲取順序是徹底遵循時間上的FIFO規則,也就是說先請求的線程必定會先獲取鎖,後來的線程確定須要排隊,這點與前面咱們分析非公平鎖的nonfairTryAcquire(int acquires)方法實現有鎖不一樣,下面是公平鎖中tryAcquire()方法的實現

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
        //判斷隊列中是否又線程在等待
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //重入鎖邏輯
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
複製代碼

2.4.三、解鎖

//ReentrantLock類的unlock
public void unlock() {
    sync.release(1);
}

//AQS類的release()方法
public final boolean release(int arg) {
    //嘗試釋放鎖
    if (tryRelease(arg)) {

        Node h = head;
        if (h != null && h.waitStatus != 0)
            //喚醒後繼結點的線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

//ReentrantLock類中的內部類Sync實現的tryRelease(int releases) 
protected final boolean tryRelease(int releases) {

      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
      boolean free = false;
      //判斷狀態是否爲0,若是是則說明已釋放同步狀態
      if (c == 0) {
          free = true;
          //設置Owner爲null
          setExclusiveOwnerThread(null);
      }
      //設置更新同步狀態
      setState(c);
      return free;
  }
複製代碼

三、ReentrantReadWriteLock

構造函數:

Lock readLock();

Lock writeLock();

/** 使用默認(非公平)的排序屬性建立一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
    this(false);
}

/** 使用給定的公平策略建立一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

複製代碼

java.util.concurrent.locks.ReentrantReadWriteLock,實現 ReadWriteLock 接口,可重入的讀寫鎖實現類。在它內部,維護了一對相關的鎖,一個用於只讀操做,另外一個用於寫入操做。只要沒有 Writer 線程,讀取鎖能夠由多個 Reader 線程同時保持。也就說說,寫鎖是獨佔的,讀鎖是共享的。

在 ReentrantLock 中,使用 Sync ( 實際是 AQS )的 int 類型的 state 來表示同步狀態,表示鎖被一個線程重複獲取的次數。可是,讀寫鎖 ReentrantReadWriteLock 內部維護着一對讀寫鎖,若是要用一個變量維護多種狀態,須要採用「按位切割使用」的方式來維護這個變量,將其切分爲兩部分:高16爲表示讀,低16爲表示寫。

分割以後,讀寫鎖是如何迅速肯定讀鎖和寫鎖的狀態呢?經過位運算。假如當前同步狀態爲S,那麼:

  • 寫狀態,等於 S & 0x0000FFFF(將高 16 位所有抹去)
  • 讀狀態,等於 S >>> 16 (無符號補 0 右移 16 位)。

一、readLock

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    
    protected final int tryAcquireShared(int unused) {
    //當前線程
    Thread current = Thread.currentThread();
    int c = getState();
    //exclusiveCount(c)計算寫鎖
    //若是存在寫鎖,且鎖的持有者不是當前線程,直接返回-1
    //存在鎖降級問題,後續闡述
    if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
        return -1;
    //讀鎖
    int r = sharedCount(c);

    /*
     * readerShouldBlock():讀鎖是否須要等待(公平鎖原則)
     * r < MAX_COUNT:持有線程小於最大數(65535)
     * compareAndSetState(c, c + SHARED_UNIT):設置讀取鎖狀態
     */
    if (!readerShouldBlock() &&
            r < MAX_COUNT &&
            compareAndSetState(c, c + SHARED_UNIT)) { //修改高16位的狀態,因此要加上2^16
        /*
         * holdCount部分後面講解
         */
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}
    
    
複製代碼

四、synchronized 和 ReentrantLock 異同?

相同點

都實現了多線程同步和內存可見性語義。
都是可重入鎖。
複製代碼

不一樣點

同步實現機制不一樣
synchronized 經過 Java 對象頭鎖標記和 Monitor 對象實現同步。
ReentrantLock 經過CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用於阻塞和解除阻塞)實現同步。


可見性實現機制不一樣
synchronized 依賴 JVM 內存模型保證包含共享變量的多線程內存可見性。
ReentrantLock 經過 ASQ 的 volatile state 保證包含共享變量的多線程內存可見性。

使用方式不一樣
synchronized 能夠修飾實例方法(鎖住實例對象)、靜態方法(鎖住類對象)、代碼塊(顯示指定鎖對象)。
ReentrantLock 顯示調用 tryLock 和 lock 方法,須要在 finally 塊中釋放鎖。

功能豐富程度不一樣
synchronized 不可設置等待時間、不可被中斷(interrupted)。
ReentrantLock 提供有限時間等候鎖(設置過時時間)、可中斷鎖(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等豐富功能

鎖類型不一樣
synchronized 只支持非公平鎖。
ReentrantLock 提供公平鎖和非公平鎖實現。固然,在大部分狀況下,非公平鎖是高效的選擇。
複製代碼
相關文章
相關標籤/搜索