1. 程式人生 > >JUC之JDK自帶鎖ReentrantReadWriteLock

JUC之JDK自帶鎖ReentrantReadWriteLock

一、Hello World!

Java紀年1.5年,ReentrantReadWriteLock誕生於J·U·C。此後,國人一般稱它為讀寫鎖。人如其名,人如其名,她就是一個可重入鎖,同時她還是一個讀、寫鎖

1.1 跟ReentrantLock並沒有親屬關係

因為ReentrantReadWriteLock在命名上跟ReetrantLock非常貼近,很容易讓人認為她跟ReentrantLock有繼承關係,其實並沒有。ReentrantReadWriteLock實現了ReadWriteLockSerializable,同時ReadWriteLockLock也沒有繼承關係。

ReentrantReadWriteLock跟ReentrantLock只有朋友關係,她們都是一個可重入鎖
但是ReentrantReadWriteLock的重入遞迴層級只有65535,即讀鎖能遞迴65535、寫鎖也同樣能夠遞迴65535層。至於為何是65535呢?在AQS框架的時候說過,AQS是用一個Integer來表示鎖的狀態。而一個Integer有32位,讀用一半(16位)、寫用一半(16位),16Bit就是65535。

1.2 對對對,我也有公平性

ReentrantReadWriteLock除與ReentrantLock一樣是具有可重入性之外,她們具有公平性。既是她們都有公平鎖

非公平鎖的實現。實現方式也差不太遠,關於公平性的內容可以翻閱JUC之JDK自帶鎖ReentrantLock

二、About Me

ReentrantReadWriteLock提供一個讀寫分離的鎖,讀鎖ReadLock控制,寫鎖WriteLock完成。當然讀與寫是互斥的。如你所知,可讀不寫,可寫不讀,即是讀寫不能同時進行,這就是讀寫鎖,與眾不同的自我。之所以能做到讀寫互斥說明她們最終還是用了同一個同步器(Sync),對的,她們Forwarding到上層(ReentrantReadWriteLock)的同步器。
我們可想而知,ReentrantReadWriteLock$Sync

ReentrantLock$Sync,她需要實現共享鎖的功能。行了,按國際慣例還是先看一下原始碼吧。對不起,原始碼太長了,我們不看原始碼了。

// 此處沒有原始碼

其實還是我認為她的原始碼簡潔優雅易懂,大家完成能自行翻閱,大體思路如下:

  1. 當讀鎖被持有,不管是被一人持有,還是多人持有,寫都需要阻塞。
  2. 當寫鎖被持有,當然只能有一個人持有了啦,此讀鎖將會被阻塞。
  3. 讀寫鎖的阻塞方式直接由公平性決定,由FairSync或NonFairSync實現。
  4. 讀鎖可以有多人同時持有,因此需要記錄持有者數量(HoldCounter)。

呵呵~,關鍵的程式碼後面涉及到了還是會拿出來看的。

2.1 讀鎖

讀鎖是一個共享鎖,即是允許多個執行緒同時獲得同一個鎖。按慣例先看一下原始碼,我們用原來的方式單個單個流程看。

// ReentrantReadWriteLock$ReadLock 原始碼節選
public static class ReadLock implements Lock, java.io.Serializable {
    private final Sync sync;

    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }

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

    public boolean tryLock() {
        return sync.tryReadLock();
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    public void unlock() {
        sync.releaseShared(1);
    }
}

這裡可以看,ReadLock的功能最終還是由上級的ReentrantReadWriteLock實現的,這點很容易看出來。

這裡的lock/tryLock/unlock總體的流程與剛剛整理過的ReentrantLock基本一樣,當然之前並沒有實現過共享模式,不過跟獨佔式差不多。


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();
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) // 非重入
        return -1;
    int r = sharedCount(c); // 讀鎖狀態
    // 不用阻塞,沒超出遞迴層級,且獲取讀鎖成功
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        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);
}

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }

        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                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++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 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);
    }
}

程式碼依然很長,這個讓我很不開心。
先來看一下fullTryAcquireShared(Thread thread),程式碼很長,邏輯很簡單。她返回值如果< 0表示當前獲取失敗,> 0表示獲取成功。

  • 當前獨佔鎖已經被持有
    自已持有了獨佔鎖的話,此時應該是會死鎖;如果不是自己持有返回 -1
  • 當前獨佔鎖沒有被持有
    1. 如果需要讀阻塞
      確定不是自己的重入,否則會發生死鎖。如果當前沒人持有返回-1
    2. 如果遞迴層級大於65535
      報錯
    3. 獲得讀鎖
      讀鎖持有者的遞迴層級+1,返回1
    4. 獲取讀鎖失敗
      如果讀鎖失敗回到原點重走一回全流程

再往上看就是tryAcquireShared(int arg)同樣返回-11表示失敗和成功。跟ReentrantLock的獲取鎖方式差不多。

  • 第一個if判斷的是是否獨佔鎖,是否是自己持有。即判定為是否非重入,非重入則返回-1
  • 第二個if判斷的是是否有條件嘗試獲取鎖,有條件則嘗試,
    1. 嘗試成功則需要記錄和統計讀鎖數量,和遞迴層級
    2. 嘗試失敗則進入上面fullTryAcquireShared(Thread)流程

如果上面tryAcquireShared(int arg)沒有成功獲取到讀鎖的話,則將需要執行doAcquireShared(int arg)繼續嘗試。
這裡有個for(;;),但它並不會使CPU飆升,因為它的實際是有阻塞的,發生在parkAndCheckInterrupt()LockSupport.park(this);完成。這裡就是迴圈嘗試和阻塞直到獲取讀鎖成功。

2.1.1 公平

讀鎖的公平模式是在當前佇列是否有獨佔鎖被持有,若已被有則讀鎖需要阻塞;否則不需要阻塞,因為讀鎖是共享鎖,只與寫鎖互斥。
這就很“公平”了,但其實這種模式的OPS比較低。

final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}
2.1.2 非公平

人如其名,apparentlyFirstQueuedIsExclusive()隊頭是不是獨佔鎖。如果是獨佔的話會,讀鎖會一直阻塞。

final boolean readerShouldBlock() {
    /* As a heuristic to avoid indefinite writer starvation,
     * block if the thread that momentarily appears to be head
     * of queue, if one exists, is a waiting writer.  This is
     * only a probabilistic effect since a new reader will not
     * block if there is a waiting writer behind other enabled
     * readers that have not yet drained from the queue.
     */
    return apparentlyFirstQueuedIsExclusive();
}

2.2 寫鎖

寫鎖依然是一個排他鎖,即是只允許一個執行緒同時持有這把鎖,也就是獨佔鎖。按國際慣例先看程式碼吧。

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

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();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

首先這裡獲取鎖失敗是指返回false,寫鎖的獲取流程跟ReentrantLock差不多。這裡需要注意的是一旦執行了 setExclusiveOwnerThread(current),之後再想嘗試讀鎖都會進入佇列排隊。

2.2.1 公平

寫鎖的公平模式跟讀鎖的公平模式差不多,非常符合我們的心理預期,所以很好理解。

final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}
2.2.2 非公平

非公平模式在寫鎖上表現比較明顯,寫鎖有總是能闖入。這個看起來可能有點超出心理預期,如果理解了會覺得更符合邏輯。

final boolean writerShouldBlock() {
    return false; // writers can always barge
}

如果是在公平的獲取策略下,其實寫鎖會永遠被阻塞,當持有讀鎖請求的話。即是讀多寫少的情況下,此時是不是寫一直拿不到鎖呢?這是肯定的,所以此時非公平模式能有更高的OPS。

三、See You

Java 1.8之前它是JDK實現的讀寫鎖的唯一實現,此後另有高人出山,高人是誰下回再說。

  • ReentrantReadWriteLock是ReadWriteLock的唯一實現,在JDK。它由讀、寫鎖組成,讀是共享鎖、寫是獨佔鎖,且讀寫互斥。
  • ReentrantLock有很多類似的地方,比如都具有可重入性、都有兩種獲取鎖的策略:公平與非公平,等。
  • 與ReentrantLock一樣在非公平模式能獲得更高的OPS。