原始碼分析:ReentrantReadWriteLock之讀寫鎖

Admol發表於2020-11-18

簡介

ReentrantReadWriteLock 從字面意思可以看出,是和重入、讀寫有關係的鎖,實際上 ReentrantReadWriteLock 確實也是支援可重入的讀寫鎖,並且支援公平和非公平獲取鎖兩種模式。

為什麼會出現讀寫鎖?

普通鎖可以保證共享資料在同一時刻只被一個執行緒訪問,就算有多個執行緒都只是讀取的操作,也還是要排隊等待獲取鎖,我們知道資料如果只涉及到讀操作,是不會出現執行緒安全方面的問題的,那這部分加鎖是不是可以去掉?或者是加鎖不互斥?如果在讀多寫少的情況下,使用普通的鎖,在所有讀的情況加鎖互斥等待會是一個及其影響系統併發量的問題,如果所有的讀操作不互斥,只有涉及到寫的時候才互斥,這樣會不會大大的提高併發量呢?答案是肯定的,ReentrantReadWriteLock 就是這樣乾的,讀讀不互斥,讀寫、寫讀、寫寫都是互斥的,可以大大提高系統併發量。

原始碼分析

類結構

ReentrantReadWriteLock 僅實現了ReadWriteLock介面

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {...}

ReadWriteLock 介面僅有兩個方法,分別是 readLock()writeLock();

主要屬性

ReentrantReadWriteLock 有3個重要的屬性,分別是讀鎖readerLock,寫鎖writerLock和同步器sync,原始碼如下:

private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;

主要內部類

  1. Sync:同步器,繼承至AbstractQueuedSynchronizer,定義了兩個抽象方法,用於兩種模式下自定義實現判斷是否要阻塞

    abstract static class Sync extends AbstractQueuedSynchronizer{
    	...
            abstract boolean readerShouldBlock();
            abstract boolean writerShouldBlock();
    	...
    }
    
  2. NonfairSync:非公平同步器,用於實現非公平鎖,繼承Sync

    static final class NonfairSync extends Sync {...}
    
  3. FairSync:公平同步器,用於實現公平鎖,繼承Sync

    static final class FairSync extends Sync {...}
    
  4. ReadLock:讀鎖,實現了Lock介面,持有同步器Sync的具體例項

    public static class ReadLock implements Lock, java.io.Serializable {
     ...
          private final Sync sync;
     ...
    }
    
  5. WriteLock:寫鎖,實現了Lock介面,持有同步器Sync的具體例項

    public static class WriteLock implements Lock, java.io.Serializable {
     ...
          private final Sync sync;
     ...
    }
    

構造方法

有兩個預設的構造方法,無參預設採用非公平鎖,有參傳入true使用公平鎖

public ReentrantReadWriteLock() {
    this(false);
}
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

獲取讀寫鎖

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

獲取讀鎖:readLock.lock()

讀鎖主要是按照共享模式來獲取鎖的,在前面講AQS的例子中——基於AQS實現自己的共享鎖,也是差不多的流程,只不過不同的鎖的實現方法tryAcquireShared有一定的區別。ReentrantReadWriteLock 讀鎖獲取過程原始碼如下:

public void lock() {
    // 共享模式獲取鎖
    sync.acquireShared(1);
}
// acquireShared 是AQS框架裡面的程式碼
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
// tryAcquireShared 是RRWLock.Sync 裡面的自己實現,所以這裡沒有公平和非公平所謂之稱
protected final int tryAcquireShared(int unused) {
    // 當前想要獲得鎖的執行緒
    Thread current = Thread.currentThread();
    // 獲取state值
    int c = getState();
    // 獨佔鎖被佔用了,並且不是當前執行緒佔有的,返回-1,出去要排隊
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1;
    // 讀鎖共享鎖的次數
    int r = sharedCount(c);
    // 判斷讀是否要阻塞,讀共享鎖的次數是否超過最大值,CAS 更新鎖state值
    // readerShouldBlock 的返回要根據同步器是否公平的具體實現來決定
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            // r==0, 設定第一次獲得讀鎖的讀者
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 持有第一個讀者讀鎖的執行緒重入計數
            firstReaderHoldCount++;
        } else {
            // 除第一個執行緒之後的其他執行緒獲得讀鎖
            // 每個執行緒每次獲得讀鎖重入計數+1
            // readHolds 就是一個ThreadLocal,裡面放的HoldCounter,用來統計每個執行緒的重入次數 
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        // 獲得讀鎖,返回1
        return 1;
    }
		// 上面if分支沒進去時,走這裡嘗試獲取讀鎖
    return fullTryAcquireShared(current);
}

上面程式碼中的readerShouldBlock()方法有兩種情況下會返回true:

  1. 公平模式下,呼叫的AQS.hasQueuedPredecessors()方法

    static final class FairSync extends Sync {
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
    
    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        // head 頭結點是當前持有鎖的節點,它的下一個節點不是當前執行緒,返回true,表示應該要阻塞當前執行緒
        return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    

    ?上面程式碼的主要思想就是:看一下隊頭排隊等待獲取鎖的執行緒是不是當前執行緒,不是的話就應該要阻塞當前執行緒;

  2. 非公平模式下,最終呼叫的AQS.apparentlyFirstQueuedIsExclusive()方法

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    }
    // apparentlyFirstQueuedIsExclusive 方法是AQS裡面的方法
    final boolean apparentlyFirstQueuedIsExclusive() {
        // h 是同步佇列的頭結點,當前持有鎖的節點
        // s 是下一個應該獲得鎖的節點
        Node h, s;
        // s 節點如果不是共享模式(在RRWLock 裡面就是讀鎖的意思),s節點是排他模式(想要寫鎖)返回true,
        return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null;
    }
    

    ?上面程式碼的主要思想就是:看一下隊頭排隊等待獲取鎖的第一個執行緒是不是要獲取寫鎖,如果是就返回true,表示要阻塞當前執行緒,當前執行緒前面還有個要獲得寫鎖的執行緒在排隊呢!如果存在這種情況,其他獲取讀鎖的執行緒都要給這種情況讓路(寫鎖優先順序更高)。那如果佇列中第一個執行緒不是要獲取寫鎖,那既然都是獲取讀鎖,那就無所謂了,允許你插隊。

上面的if分支進入失敗時,會進入到fullTryAcquireShared()方法再次嘗試獲得讀鎖有3種情況會進入到這個方法:

  1. readerShouldBlock() 方法返回true,上面已經分析了,這個方法什麼時候會返回true
  2. 共享計數達到了最大值 MAX_COUNT(65535),可能性較小
  3. CAS 修改state 值失敗,也就是獲取鎖失敗

下面是 fullTryAcquireShared() 方法的分析:

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    // 自旋
    for (;;) {
        int c = getState();
        // != 0 已經有其他執行緒獲得了寫鎖
        if (exclusiveCount(c) != 0) {
            // 如果不是當前執行緒獲得的寫鎖,返回-1,出去阻塞排隊
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // 要進入到這個分支,說明exclusiveCount(c) == 0 , 也就是寫鎖沒被佔用
            //  readerShouldBlock() == true , 公平模式下,同步佇列中有其他執行緒在排隊,非公平模式下,有即將要獲得寫鎖的執行緒
            //  readerShouldBlock() 返回true ,也就是要阻塞當前執行緒的意思
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                // 進入到這裡,說明第一個讀鎖不是當前執行緒獲得的
                // rh 可以理解為當前執行緒的重入計數
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 返回-1,阻塞當前執行緒,出去排隊
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            // 超讀鎖上限,丟擲錯誤
            throw new Error("Maximum lock count exceeded");
        // 進入到這兒,說明執行緒沒有其他執行緒獲得了寫鎖,並且不需要阻塞當前執行緒
        // 再次嘗試CAS 獲得鎖,CAS 修改失敗會繼續自旋進行
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 成功獲得鎖
            if (sharedCount(c) == 0) {
                // 第一個獲得讀鎖的執行緒
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                // 第一個獲得讀鎖的執行緒重入計數+1
                firstReaderHoldCount++;
            } else {
                // 非第一個獲得讀鎖的執行緒 
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                // 執行緒重入計數
                rh.count++;
                // 快取成功獲取readLock的最後一個執行緒的計數
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

如果上面fullTryAcquireShared()方法還是沒有獲得鎖,返回-1,就會進入下面的doAcquireShared(int arg)方法:

// doAcquireShared 方法是AQS裡面的程式碼,非RRWLock 實現
private void doAcquireShared(int arg) {
    // 新增一個共享模式的節點到同步佇列,並返回當前節點
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        // 中斷標識
        boolean interrupted = false;
        // for迴圈自旋操作
        for (;;) {
            // 在同步佇列中,當前節點的前驅結點
            final Node p = node.predecessor();
            if (p == head) {
                // 如果前驅結點是頭結點,說明排隊輪到當前節點獲得鎖
                // tryAcquireShared 再次嘗試獲取鎖,上面的邏輯一模一樣
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // >=0 說明成功獲得了鎖 
                    // 設定新的頭結點,並檢查後面是否是在獲得讀鎖,如果是就喚醒它
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        // 阻塞期間執行緒被中斷了
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 阻塞中斷執行緒
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private void setHeadAndPropagate(Node node, int propagate) {
    // 舊的頭結點
    Node h = head; // Record old head for check below
    // 獲得鎖的執行緒節點設定為新的頭結點
    setHead(node);
    if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 檢查獲得鎖的下一個節點s是否是共享模式的節點(讀)
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

private void doReleaseShared() {
    // 自旋
    for (;;) {
        Node h = head;
        // 同步佇列不為空
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // -1 :表示當前節點的後繼節點包含的執行緒需要執行,也就是unpark
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 喚醒被阻塞的下一個節點
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 只會喚醒一個節點,在呼叫上面程式碼過程中,如果head節點變了,就會一直自旋,直到成功
        if (h == head)                   // loop if head changed
            break;
    }
}

獲取讀鎖過程總結

  1. 嘗試去獲取鎖tryAcquireShared()
  2. tryAcquireShared() 中成功獲得鎖,就直接退出,執行lock() 之後的程式碼邏輯
    1. 如果有其他執行緒已經佔用了寫鎖,退出方法,返回-1,獲取鎖失敗
    2. 檢查是否要阻塞當前的執行緒readerShouldBlock(),有兩種情況下(也就是公平鎖和非公平鎖獲取讀鎖的區別)會阻塞當前現在:
      1. 如果是公平鎖,會看一下隊頭排隊等待獲取鎖的執行緒是不是當前執行緒,不是的話就應該要阻塞當前執行緒;公平模式下是不允許插隊的!
      2. 如果是非公平鎖,看一下隊頭排隊等待獲取鎖的第一個執行緒是不是要獲取寫鎖,如果是表示要阻塞當前執行緒,寫鎖優先順序更高!
    3. 檢查讀鎖計數是否已經到了最大值(65535)
    4. 上面檢查通過,才嘗試CAS 修改同步狀態,修改成功,代表成功獲取讀鎖,退出方法返回1
      1. 成功獲取讀鎖,如果是第一個獲得讀鎖的執行緒,會快取該執行緒firstReader,如果是重入,會進行重入計數,如果是新的執行緒獲得讀鎖,會用一個ThreadLocal來儲存重入計數
    5. 如果到上面還沒獲取到鎖(可能是CAS修改同步狀態失敗),會進行自旋繼續嘗試獲取鎖,對應方法fullTryAcquireShared() ,該方法要麼獲取鎖成功,要麼獲取鎖失敗,直到退出整個tryAcquireShared() 方法
  3. 如果tryAcquireShared() 中沒有獲得鎖,進入到AQS的doAcquireShared方法,排隊、阻塞執行緒
    1. doAcquireShared 方法也是一個自旋的操作,沒有獲取到鎖,就會阻塞執行緒,等待被喚醒後繼續獲取鎖,知道獲取鎖成功為止

釋放讀鎖:readLock.lock()

讀鎖釋放鎖的邏輯如下:

public void unlock() {
    // 開始釋放讀鎖
    sync.releaseShared(1);
}
//AQS框架中 的方法
public final boolean releaseShared(int arg) {
    // tryReleaseShared 在RRWLock 中的Sync裡面
    if (tryReleaseShared(arg)) {
        // 喚醒後面的讀鎖節點
        doReleaseShared();
        return true;
    }
    return false;
}
// RRWLock.Sync 的實現方法
protected final boolean tryReleaseShared(int unused) {
    // 當前執行緒
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // 第一個讀鎖執行緒 
        if (firstReaderHoldCount == 1)
            // 如果它只獲得了一次鎖,直接置為null
            firstReader = null;
        else
            // 第一個執行緒獲得讀鎖,並且重入獲取鎖很多次,慢慢減,直到為1,置為null
            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;
    }
    // 上面只是減重入的計數
    // 下面是自旋,重置同步狀態state值
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc)) 
            // CAS 修改成功,並且要state為0才是真正釋放了讀鎖
            // 如果有重入,只有釋放最後一次才會返回true, 之後才會去嘗試喚醒之後的節點
            return nextc == 0;
    }
}

private void doReleaseShared() {
    // 自旋
    for (;;) {
        Node h = head;
        // 同步等待的佇列不為空
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 檢查狀態是否要喚醒下一個節點的執行緒
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 加入h節點是持有鎖的節點,會喚醒它的下一個節點執行緒
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 理論上喚醒一個就會退出
        if (h == head)                   // loop if head changed
            break;
    }
}

釋放讀鎖過程總結:

  1. 減計數,包含執行緒重入獲取鎖的計數
    • 從這裡可以看出一個執行緒存在多次釋放鎖,會丟擲異常
  2. 自旋,CAS 修改同步狀態,重入獲取鎖的執行緒只有在state等於0時才是真正的釋放鎖成功
  3. 釋放鎖成功後,會喚醒佇列中的下一個節點,下一個節點會繼續獲取鎖

獲取寫鎖:writeLock.lock()

public void lock() {
    sync.acquire(1);
}

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
    // 當前執行緒
    Thread current = Thread.currentThread();
    int c = getState();
    // 寫鎖計數,>0的話說明寫鎖已經被佔用了
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // c != 0 and w == 0 可能共享鎖已經被佔用了,這時候寫鎖獲取失敗
        // 同一個執行緒先獲取讀鎖,再獲取寫鎖,也會在這裡返回false,獲取寫鎖出去之後會阻塞自己,
        // 然後自己的讀鎖也不會釋放,其他執行緒也獲取不了讀鎖,就出現了死鎖
        if (w == 0 || current != getExclusiveOwnerThread())
            // c != 0 and w == 0  鎖的持有者不是當前執行緒,返回false
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            // 超限了 65535
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        // 重入獲取鎖,計數+1
        setState(c + acquires);
        return true;
    }
    //  writerShouldBlock的實現程式碼,以看上面讀鎖獲取readerShouldBlock的分析
    // 公平鎖時,writerShouldBlock 呼叫的hasQueuedPredecessors()
    // 非公平鎖時,只返回false
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        // CAS  修改失敗,返回false
        return false;
    // 成功獲取寫鎖,設定鎖的擁有者執行緒
    setExclusiveOwnerThread(current);
    return true;
}

如果上面方法沒有獲取到寫鎖,會執行acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ,這塊的程式碼分析,可以檢視之前的文章,關於AQS的分析或者ReentrantLock的分析。

釋放寫鎖:writeLock.unlock()

釋放寫鎖的邏輯比較簡單,一般加鎖和解鎖都是成對出現的,所以這裡解鎖並不需要同步互斥的手段來進行,原始碼如下:

public void unlock() {
    sync.release(1);
}
// AQS 框架的程式碼
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    // 校驗是否是當前執行緒持有寫鎖
    if (!isHeldExclusively())
        // 釋放別人的寫鎖,丟擲異常
        throw new IllegalMonitorStateException();
    // 計算下一個同步狀態值
    int nextc = getState() - releases;
    // 重入的情況,是否已經完全釋放了
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        // 完全釋放了,設定鎖的持有者執行緒
        setExclusiveOwnerThread(null);
    // 
    setState(nextc);
    return free;
}

完全釋放鎖成功後,喚醒下一個節點的邏輯在AQS的unparkSuccessor程式碼中,不需要RRWLock來實現。

死鎖問題

在上面獲取寫鎖的過程中,分析了同一個執行緒先獲取讀鎖,再獲取寫鎖,寫鎖的邏輯會阻塞自己的執行緒,但是寫鎖和讀鎖又是同一個執行緒,相當於前面的寫鎖也被阻塞了,這時候寫鎖沒地方釋放,讀鎖也沒有地方釋放,其他執行緒讀鎖和寫鎖也都獲取不了了,因為前面有個寫鎖在排隊獲取。

public static void main(String[] args) throws InterruptedException{
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
    Lock writeLock = lock.writeLock();
    Lock readLock = lock.readLock();
    new Thread(new Runnable(){
        @SneakyThrows
        @Override
        public void run(){
            TimeUnit.SECONDS.sleep(1);
            // 模擬1秒後其他執行緒來獲得讀鎖
            System.out.println(Thread.currentThread().getName()+":準備獲得讀鎖");
            readLock.lock();
            System.out.println(Thread.currentThread().getName()+":執行緒獲得讀鎖");
            readLock.unlock();
            System.out.println(Thread.currentThread().getName()+":釋放了讀鎖");
        }
    },"T0").start();
    readLock.lock();
    System.out.println(Thread.currentThread().getName()+":獲得了讀鎖");
    writeLock.lock();
    System.out.println(Thread.currentThread().getName()+":獲得了寫鎖");
    readLock.unlock();
    System.out.println(Thread.currentThread().getName()+":解讀鎖");
    writeLock.unlock();
    System.out.println(Thread.currentThread().getName()+":解寫鎖");
}

輸出結果:

main:獲得了讀鎖
T0:準備獲得讀鎖

從上面輸出結果可以看出,只有main執行緒獲得了讀鎖,自己獲取寫鎖被阻塞,其他執行緒也獲取不了讀鎖,最後產生了死鎖。

寫執行緒飢餓問題

ReentrantReadWriteLock 的讀寫是互斥的,意思就是讀鎖在獲取鎖後,在還沒有釋放鎖的期間,獲取寫鎖的程式來了也要阻塞自己排隊,如果有大量的執行緒獲取了讀鎖,之後有一個執行緒獲取寫鎖,寫鎖就可能一直獲取不到寫鎖,引起寫鎖執行緒“飢餓”,這就是RRWLock的寫執行緒飢餓問題。

我們用程式碼來驗證一下上面的結論:

private static void testWriteLockHunger() throws InterruptedException{
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
    Lock writeLock = lock.writeLock();
    Lock readLock = lock.readLock();
    // T0 執行緒先獲得讀鎖,並持有一段時間
    new Thread(new Runnable(){
        @SneakyThrows
        @Override
        public void run(){
            readLock.lock();
            System.out.println(Thread.currentThread().getName()+":最開始執行緒獲得讀鎖");
            // 睡眠15秒,一直持有讀鎖
            TimeUnit.SECONDS.sleep(15);
            readLock.unlock();
            System.out.println(Thread.currentThread().getName()+":釋放了讀鎖");
        }
    },"T0").start();
    // 1秒後其他執行緒再來獲取鎖,保證前面那個T0執行緒最先獲得讀鎖
    TimeUnit.SECONDS.sleep(1);
    // TW-1 來排隊獲取寫鎖,是為了讓後面的讀鎖,寫鎖都入隊排隊
    new Thread(new Runnable(){
        @SneakyThrows
        @Override
        public void run(){
            System.out.println(Thread.currentThread().getName()+":準備獲得寫鎖");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName()+":獲得寫鎖");
            TimeUnit.SECONDS.sleep(5);
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName()+":釋放了寫鎖");
        }
    },"TW-1").start();
    TimeUnit.SECONDS.sleep(1);
    // 這裡睡眠1秒是為了寫鎖排隊在讀鎖獲取的前面
    IntStream.range(1,5).forEach(i->{
        new Thread(new Runnable(){
            @SneakyThrows
            @Override
            public void run(){
                System.out.println(Thread.currentThread().getName()+":準備獲取讀鎖");
                readLock.lock();
                System.out.println(Thread.currentThread().getName()+":獲取了讀鎖");
                // 持有部分時間的讀鎖
                TimeUnit.SECONDS.sleep(i*2);
                readLock.unlock();
                System.out.println(Thread.currentThread().getName()+":釋放了讀鎖");
            }
        },"T-"+i).start();
    });
    // 最後再來個獲取寫鎖的執行緒,肯定會在所有讀鎖的後面獲取到寫鎖
    new Thread(new Runnable(){
        @SneakyThrows
        @Override
        public void run(){
            System.out.println(Thread.currentThread().getName()+":準備獲取寫鎖");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName()+":獲取了寫鎖");
            // 持有部分時間的讀鎖
            TimeUnit.SECONDS.sleep(2);
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName()+":釋放了寫鎖");
        }
    },"TW").start();
}

上面程式碼輸出示例:

T0:最開始執行緒獲得讀鎖
TW-1:準備獲得寫鎖
T-1:準備獲取讀鎖
T-2:準備獲取讀鎖
T-4:準備獲取讀鎖
T-3:準備獲取讀鎖
TW:準備獲取寫鎖
T0:釋放了讀鎖
TW-1:獲得寫鎖
TW-1:釋放了寫鎖
T-1:獲取了讀鎖
T-2:獲取了讀鎖
T-4:獲取了讀鎖
T-3:獲取了讀鎖
T-1:釋放了讀鎖
T-2:釋放了讀鎖
T-3:釋放了讀鎖
T-4:釋放了讀鎖
TW:獲取了寫鎖
TW:釋放了寫鎖

從上面輸出結果可以看出,TW寫鎖是最後才獲取到寫鎖的,如果前面有大量的讀鎖在排隊的話,寫鎖肯定就會造成飢餓的。

如果不想讓獲取寫鎖的執行緒“飢餓”怎麼辦呢?

可以把最後獲取寫鎖的執行緒TW獲取鎖方式改造下,程式碼如下:

new Thread(new Runnable(){
    @SneakyThrows
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName()+":準備獲取寫鎖");
        while(!writeLock.tryLock()){
            // 一直嘗試獲得寫鎖,直到成功
        }
        System.out.println(Thread.currentThread().getName()+":獲取了寫鎖");
        // 持有部分時間的讀鎖
        TimeUnit.SECONDS.sleep(2);
        writeLock.unlock();
        System.out.println(Thread.currentThread().getName()+":釋放了寫鎖");
    }
},"TW").start();

測試輸出結果:

T0:最開始執行緒獲得讀鎖
TW-1:準備獲得寫鎖
T-1:準備獲取讀鎖
T-2:準備獲取讀鎖
T-3:準備獲取讀鎖
T-4:準備獲取讀鎖
TW:準備獲取寫鎖
T0:釋放了讀鎖
TW-1:獲得寫鎖
TW-1:釋放了寫鎖
TW:獲取了寫鎖
TW:釋放了寫鎖
T-4:獲取了讀鎖
T-2:獲取了讀鎖
T-3:獲取了讀鎖
T-1:獲取了讀鎖
T-1:釋放了讀鎖
T-2:釋放了讀鎖
T-3:釋放了讀鎖
T-4:釋放了讀鎖

從上面輸出結果可以看出,TW執行緒成功的在讀鎖前面獲取到了寫鎖;那為什麼會這樣呢?因為採用lock()來獲取鎖,如果第一次tryAcquire沒有獲取到鎖,就會被加入到佇列等待,只要進入了佇列,就只能按照佇列中的順序來獲得鎖了,而tryLock在獲取鎖失敗後是不會加入到同步等待佇列中去的,從而實現“插隊”的功能。

總結

  1. 讀寫鎖除了讀讀不互斥,讀寫、寫讀、寫寫都是互斥的。
  2. 讀寫互斥的意思是A執行緒先獲取讀鎖不釋放,B來獲取寫鎖,這時候B執行緒一樣的要阻塞自己
  3. 同一個執行緒先獲取讀鎖,再獲取寫鎖,會導致死鎖
  4. 允許同一個執行緒先獲取寫鎖,再獲取讀鎖;但是不允許同一個執行緒先獲取讀鎖,再獲取寫鎖;可以理解為允許鎖降級,不允許鎖升級。
  5. 公平鎖模式下,獲取寫鎖會去檢查佇列中是否有排隊更久的執行緒。
  6. 非公平鎖模式下,獲取寫鎖不會去檢查同步佇列中是否有排隊更久的執行緒。
  7. 公平鎖模式下,獲取讀鎖會去檢查佇列中是否有排隊更久的執行緒。
  8. 非公平鎖模式下,獲取讀鎖會去檢查佇列中第一個等待獲取的是不是寫鎖,如果存在就要阻塞當前獲取讀鎖的執行緒(寫鎖優先順序更高)。

相關文章