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$SyncReentrantLock$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。
    原文作者:JUC
    原文地址: https://blog.csdn.net/zteny/article/details/55113799
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞