Java高階特性增強-鎖

王知無發表於2019-02-19

請戳GitHub原文: github.com/wangzhiwubi…

大資料成神之路系列:

請戳GitHub原文: github.com/wangzhiwubi…

Java高階特性增強-集合

Java高階特性增強-多執行緒

Java高階特性增強-Synchronized

Java高階特性增強-volatile

Java高階特性增強-併發集合框架

Java高階特性增強-分散式

Java高階特性增強-Zookeeper

Java高階特性增強-JVM

Java高階特性增強-NIO

公眾號

  • 全網唯一一個從0開始幫助Java開發者轉做大資料領域的公眾號~

  • 公眾號大資料技術與架構或者搜尋import_bigdata關注,大資料學習路線最新更新,已經有很多小夥伴加入了~

Java高階特性增強-鎖

Java高階特性增強-鎖

本部分網路上有大量的資源可以參考,在這裡做了部分整理,感謝前輩的付出,每節文章末尾有引用列表,原始碼推薦看JDK1.8以後的版本,注意甄別~ ####多執行緒 ###集合框架 ###NIO ###Java併發容器


Java中的鎖分類

在讀很多併發文章中,會提及各種各樣鎖如公平鎖,樂觀鎖等等,這篇文章介紹就是各種鎖。介紹的內容如下: 公平鎖/非公平鎖 可重入鎖 獨享鎖/共享鎖 互斥鎖/讀寫鎖 樂觀鎖/悲觀鎖 分段鎖 偏向鎖/輕量級鎖/重量級鎖 自旋鎖 上面是很多鎖的名詞,這些分類並不是全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計,下面總結的內容是對每個鎖的名詞進行一定的解釋。 公平鎖/非公平鎖 公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖。 非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖。有可能,會造成優先順序反轉或者飢餓現象。 對於Java ReentrantLock而言,通過建構函式指定該鎖是否是公平鎖,預設是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。 對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的來實現執行緒排程,所以並沒有任何辦法使其變成公平鎖。

可重入鎖 可重入鎖又名遞迴鎖,是指在同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。說的有點抽象,下面會有一個程式碼的示例。 對於Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖,其名字是Re entrant Lock重新進入鎖。 對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}
複製程式碼

上面的程式碼就是一個可重入鎖的一個特點,如果不是可重入鎖的話,setB可能不會被當前執行緒執行,可能造成死鎖。

獨享鎖/共享鎖 獨享鎖是指該鎖一次只能被一個執行緒所持有。 共享鎖是指該鎖可被多個執行緒所持有。

對於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。

Lock介面

在Lock介面出現之前,Java程式是靠synchronized關鍵字實現鎖功能的。JDK1.5之後併發包中新增了Lock介面以及相關實現類來實現鎖功能。

雖然synchronized方法和語句的範圍機制使得使用監視器鎖更容易程式設計,並且有助於避免涉及鎖的許多常見程式設計錯誤,但是有時您需要以更靈活的方式處理鎖。例如,用於遍歷併發訪問的資料結構的一些演算法需要使用“手動”或“鏈鎖定”:您獲取節點A的鎖定,然後獲取節點B,然後釋放A並獲取C,然後釋放B並獲得D等。在這種場景中synchronized關鍵字就不那麼容易實現了,使用Lock介面容易很多。

Lock介面的實現類: ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock

AbstractQueuedSynchronizer

當你檢視原始碼時你會驚訝的發現ReentrantLock並沒有多少程式碼,另外有一個很明顯的特點是:基本上所有的方法的實現實際上都是呼叫了其靜態記憶體類Sync中的方法,而Sync類繼承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock關鍵核心在於對佇列同步器AbstractQueuedSynchronizer(簡稱同步器)的理解。

在同步元件的實現中,AQS是核心部分,同步元件的實現者通過使用AQS提供的模板方法實現同步元件語義,AQS則實現了對同步狀態的管理,以及對阻塞執行緒進行排隊,等待通知等等一些底層的實現處理。AQS的核心也包括了這些方面:同步佇列,獨佔式鎖的獲取和釋放,共享鎖的獲取和釋放以及可中斷鎖,超時等待鎖獲取這些特性的實現,而這些實際上則是AQS提供出來的模板方法,歸納整理如下: 獨佔式鎖:

void acquire(int arg):
獨佔式獲取同步狀態,如果獲取失敗則插入同步佇列進行等待;
void acquireInterruptibly(int arg):
與acquire方法相同,但在同步佇列中進行等待的時候可以檢測中斷;
boolean tryAcquireNanos(int arg, long nanosTimeout):
在acquireInterruptibly基礎上增加了超時等待功能,在超時時間內沒有獲得同步狀態返回false;
boolean release(int arg):
釋放同步狀態,該方法會喚醒在同步佇列中的下一個節點
複製程式碼

共享式鎖:

void acquireShared(int arg):
共享式獲取同步狀態,與獨佔式的區別在於同一時刻有多個執行緒獲取同步狀態
void acquireSharedInterruptibly(int arg):
在acquireShared方法基礎上增加了能響應中斷的功能
boolean tryAcquireSharedNanos(int arg, long nanosTimeout):
在acquireSharedInterruptibly基礎上增加了超時等待的功能
boolean releaseShared(int arg):共享式釋放同步狀態
複製程式碼
ReentrantLock

ReentrantLock重入鎖,是實現Lock介面的一個類,也是在實際程式設計中使用頻率很高的一個鎖,支援重入性,表示能夠對共享資源能夠重複加鎖,即當前執行緒獲取該鎖再次獲取不會被阻塞。在java關鍵字synchronized隱式支援重入性,synchronized通過獲取自增,釋放自減的方式實現重入。與此同時,ReentrantLock還支援公平鎖和非公平鎖兩種方式。那麼,要想完完全全的弄懂ReentrantLock的話,主要也就是ReentrantLock同步語義的學習:1. 重入性的實現原理;2. 公平鎖和非公平鎖。

重入性的實現原理

要想支援重入性,就要解決兩個問題:1. 線上程獲取鎖的時候,如果已經獲取鎖的執行緒是當前執行緒的話則直接再次獲取成功;2. 由於鎖會被獲取n次,那麼只有鎖在被釋放同樣的n次之後,該鎖才算是完全釋放成功。通過這篇文章,我們知道,同步元件主要是通過重寫AQS的幾個protected方法來表達自己的同步語義。針對第一個問題,我們來看看ReentrantLock是怎樣實現的,以非公平鎖為例,判斷當前執行緒能否獲得鎖為例,核心方法為nonfairTryAcquire:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //1. 如果該鎖未被任何執行緒佔有,該鎖能被當前執行緒獲取
	if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
	//2.若被佔有,檢查佔有執行緒是否是當前執行緒
    else if (current == getExclusiveOwnerThread()) {
		// 3. 再次獲取,計數加一
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

複製程式碼

這段程式碼的邏輯也很簡單,具體請看註釋。為了支援重入性,在第二步增加了處理邏輯,如果該鎖已經被執行緒所佔有了,會繼續檢查佔有執行緒是否為當前執行緒,如果是的話,同步狀態加1返回true,表示可以再次獲取成功。每次重新獲取都會對同步狀態進行加一的操作,那麼釋放的時候處理思路是怎樣的了?(依然還是以非公平鎖為例)核心方法為tryRelease:

protected final boolean tryRelease(int releases) {
	//1. 同步狀態減1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
		//2. 只有當同步狀態為0時,鎖成功被釋放,返回true
        free = true;
        setExclusiveOwnerThread(null);
    }
	// 3. 鎖未被完全釋放,返回false
    setState(c);
    return free;
}

複製程式碼

程式碼的邏輯請看註釋,需要注意的是,重入鎖的釋放必須得等到同步狀態為0時鎖才算成功釋放,否則鎖仍未釋放。如果鎖被獲取n次,釋放了n-1次,該鎖未完全釋放返回false,只有被釋放n次才算成功釋放,返回true。到現在我們可以理清ReentrantLock重入性的實現了,也就是理解了同步語義的第一條.

公平鎖與非公平鎖

ReentrantLock支援兩種鎖:公平鎖和非公平鎖。何謂公平性,是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求上的絕對時間順序,滿足FIFO。ReentrantLock的構造方法無參時是構造非公平鎖,原始碼為:

public ReentrantLock() {
    sync = new NonfairSync();
}
複製程式碼

另外還提供了另外一種方式,可傳入一個boolean值,true時為公平鎖,false時為非公平鎖,原始碼為:

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

在上面非公平鎖獲取時(nonfairTryAcquire方法)只是簡單的獲取了一下當前狀態做了一些邏輯處理,並沒有考慮到當前同步佇列中執行緒等待的情況。我們來看看公平鎖的處理邏輯是怎樣的,核心方法為:

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;
  }
}
複製程式碼

這段程式碼的邏輯與nonfairTryAcquire基本上一直,唯一的不同在於增加了hasQueuedPredecessors的邏輯判斷,方法名就可知道該方法用來判斷當前節點在同步佇列中是否有前驅節點的判斷,如果有前驅節點說明有執行緒比當前執行緒更早的請求資源,根據公平性,當前執行緒請求資源失敗。如果當前節點沒有前驅節點的話,再才有做後面的邏輯判斷的必要性。公平鎖每次都是從同步佇列中的第一個節點獲取到鎖,而非公平性鎖則不一定,有可能剛釋放鎖的執行緒能再次獲取到鎖。

公平鎖 VS 非公平鎖

公平鎖每次獲取到鎖為同步佇列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的執行緒下次繼續獲取該鎖,則有可能導致其他執行緒永遠無法獲取到鎖,造成“飢餓”現象。

公平鎖為了保證時間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會降低一定的上下文切換,降低效能開銷。因此,ReentrantLock預設選擇的是非公平鎖,則是為了減少一部分上下文切換,保證了系統更大的吞吐量。

ReentrantReadWriteLock

在併發場景中用於解決執行緒安全的問題,我們幾乎會高頻率的使用到獨佔式鎖,通常使用java提供的關鍵字synchronized或者concurrents包中實現了Lock介面的ReentrantLock。它們都是獨佔式獲取鎖,也就是在同一時刻只有一個執行緒能夠獲取鎖。而在一些業務場景中,大部分只是讀資料,寫資料很少,如果僅僅是讀資料的話並不會影響資料正確性(出現髒讀),而如果在這種業務場景下,依然使用獨佔鎖的話,很顯然這將是出現效能瓶頸的地方。針對這種讀多寫少的情況,java還提供了另外一個實現Lock介面的ReentrantReadWriteLock(讀寫鎖)。讀寫所允許同一時刻被多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他的寫執行緒都會被阻塞。在分析WirteLock和ReadLock的互斥性時可以按照WriteLock與WriteLock之間,WriteLock與ReadLock之間以及ReadLock與ReadLock之間進行分析。這裡做一個歸納總結:

公平性選擇:支援非公平性(預設)和公平的鎖獲取方式,吞吐量還是非公平優於公平; 重入性:支援重入,讀鎖獲取後能再次獲取,寫鎖獲取之後能夠再次獲取寫鎖,同時也能夠獲取讀鎖; 鎖降級:遵循獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖

要想能夠徹底的理解讀寫鎖必須能夠理解這樣幾個問題:1. 讀寫鎖是怎樣實現分別記錄讀寫狀態的?2. 寫鎖是怎樣獲取和釋放的?3.讀鎖是怎樣獲取和釋放的?我們帶著這樣的三個問題,再去了解下讀寫鎖。

寫鎖詳解

寫鎖的獲取

同步元件的實現聚合了同步器(AQS),並通過重寫重寫同步器(AQS)中的方法實現同步元件的同步語義。因此,寫鎖的實現依然也是採用這種方式。在同一時刻寫鎖是不能被多個執行緒所獲取,很顯然寫鎖是獨佔式鎖,而實現寫鎖的同步語義是通過重寫AQS中的tryAcquire方法實現的。原始碼為:

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
	// 1. 獲取寫鎖當前的同步狀態
    int c = getState();
	// 2. 獲取寫鎖獲取的次數
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
		// 3.1 當讀鎖已被讀執行緒獲取或者當前執行緒不是已經獲取寫鎖的執行緒的話
		// 當前執行緒獲取寫鎖失敗
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
		// 3.2 當前執行緒獲取寫鎖,支援可重複加鎖
        setState(c + acquires);
        return true;
    }
	// 3.3 寫鎖未被任何執行緒獲取,當前執行緒可獲取寫鎖
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}
複製程式碼

這段程式碼的邏輯請看註釋,這裡有一個地方需要重點關注,exclusiveCount(c)方法,該方法原始碼為:

static int exclusiveCount(int c) { 
       return c & EXCLUSIVE_MASK; 
 }
複製程式碼

其中EXCLUSIVE_MASK為: static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
EXCLUSIVE_MASK為1左移16位然後減1,即為0x0000FFFF。而exclusiveCount方法是將同步狀態(state為int型別)與0x0000FFFF相與,即取同步狀態的低16位。那麼低16位代表什麼呢?根據exclusiveCount方法的註釋為獨佔式獲取的次數即寫鎖被獲取的次數,現在就可以得出來一個結論同步狀態的低16位用來表示寫鎖的獲取次數。同時還有一個方法值得我們注意:

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
複製程式碼

該方法是獲取讀鎖被獲取的次數,是將同步狀態(int c)右移16次,即取同步狀態的高16位,現在我們可以得出另外一個結論同步狀態的高16位用來表示讀鎖被獲取的次數。現在還記得我們開篇說的需要弄懂的第一個問題嗎?讀寫鎖是怎樣實現分別記錄讀鎖和寫鎖的狀態的,現在這個問題的答案就已經被我們弄清楚了。 現在我們回過頭來看寫鎖獲取方法tryAcquire,其主要邏輯為:當讀鎖已經被讀執行緒獲取或者寫鎖已經被其他寫執行緒獲取,則寫鎖獲取失敗;否則,獲取成功並支援重入,增加寫狀態。

寫鎖的釋放 寫鎖釋放通過重寫AQS的tryRelease方法,原始碼為:

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
	//1. 同步狀態減去寫狀態
    int nextc = getState() - releases;
	//2. 當前寫狀態是否為0,為0則釋放寫鎖
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
	//3. 不為0則更新同步狀態
    setState(nextc);
    return free;
}

複製程式碼

原始碼的實現邏輯請看註釋,不難理解與ReentrantLock基本一致,這裡需要注意的是,減少寫狀態int nextc = getState() - releases;只需要用當前同步狀態直接減去寫狀態的原因正是我們剛才所說的寫狀態是由同步狀態的低16位表示的。

讀鎖詳解

讀鎖的獲取 看完了寫鎖,現在來看看讀鎖,讀鎖不是獨佔式鎖,即同一時刻該鎖可以被多個讀執行緒獲取也就是一種共享式鎖。按照之前對AQS介紹,實現共享式同步元件的同步語義需要通過重寫AQS的tryAcquireShared方法和tryReleaseShared方法。讀鎖的獲取實現方法為:

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
	//1. 如果寫鎖已經被獲取並且獲取寫鎖的執行緒不是當前執行緒的話,當前
	// 執行緒獲取讀鎖失敗返回-1
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
		//2. 當前執行緒獲取讀鎖
        compareAndSetState(c, c + SHARED_UNIT)) {
		//3. 下面的程式碼主要是新增的一些功能,比如getReadHoldCount()方法
		//返回當前獲取讀鎖的次數
        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;
    }
	//4. 處理在第二步中CAS操作失敗的自旋已經實現重入性
    return fullTryAcquireShared(current);
}

複製程式碼

程式碼的邏輯請看註釋,需要注意的是 當寫鎖被其他執行緒獲取後,讀鎖獲取失敗,否則獲取成功利用CAS更新同步狀態。另外,當前同步狀態需要加上SHARED_UNIT((1 << SHARED_SHIFT)即0x00010000)的原因這是我們在上面所說的同步狀態的高16位用來表示讀鎖被獲取的次數。如果CAS失敗或者已經獲取讀鎖的執行緒再次獲取讀鎖時,是靠fullTryAcquireShared方法實現的,有興趣可以看看。

讀鎖的釋放 讀鎖釋放的實現主要通過方法tryReleaseShared,原始碼如下,主要邏輯請看註釋:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
	// 前面還是為了實現getReadHoldCount等新功能
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        int c = getState();
		// 讀鎖釋放 將同步狀態減去讀狀態即可
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}

複製程式碼
鎖降級

讀寫鎖支援鎖降級,遵循按照獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖,不支援鎖升級,關於鎖降級下面的示例程式碼摘自ReentrantWriteReadLock原始碼中:

void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // Recheck state because another thread might have
                // acquired write lock and changed state before we did.
                if (!cacheValid) {
                    data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
      }
 
      try {
        use(data);
      } finally {
        rwl.readLock().unlock();
      }
    }
}
複製程式碼

參考文章和書籍:

《Java併發程式設計的藝術》 《實戰Java高併發程式設計》 blog.csdn.net/qq_34337272… www.jianshu.com/p/a5f99f253… www.jianshu.com/p/506c1e38a…

Java高階特性增強-鎖

    請戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData

                   關注公眾號,內推,面試,資源下載,關注更多大資料技術~
                   大資料成神之路~預計更新500+篇文章,已經更新50+篇~ 複製程式碼

相關文章