AI工具人
提示词工程师

ReentrantReadWriteLock源码解析

上回说到ReentrantLock,今天来谈谈读写锁(ReentrantLock)和其具体实现ReentrantReadWriteLock。看这篇文章前,强烈建议你回到先读懂ReentrantLock,因为ReentrantReadWriteLock其实是在ReentrantLock的基础上实现的,可以参考我之前的博客ReentrantLock源码解析

既然有了锁,为什么还需要读写锁?我们来想象下这个场景。你们小区楼下有个公告栏,有时候有人会写个招租,有时候有人会写个寻物启事…… 当然一个人正在改公告栏的时候,另外一个人就不能同时改了,这里就相当于有了一把无形的锁,我改的时候就把广告栏“锁住”,改完再“解锁”,当然别人锁住了之后我也改不了。说完了“写”再说“读”,一个人在读公告栏的时候,别人就不能去写了,这样不礼貌,这里也相当于读的人用一把“锁”把公告栏给锁了。

如果这里读者用的锁和写者用的锁是一样的,那么这把锁不紧不然别人写了,也不让别人读了,相当于一个人在看公告栏,别人就不能看了,这明显不合理啊。 所以要把读和写用的锁区分开来,所有读的人共享一把锁,写的人独享锁。放到公告栏的例子上,改公告的时候同时只有一个人可以看,但读的时候所有人可以同时读,这样就可以把“公告栏”这个资源的利用率最大化。

看到这里,你应该已经理解了什么叫做“读写锁”,接下来我们直接看下jdk中ReentrantReadWriteLock的实现,再次建议先阅读ReentrantLock的具体实现。
在这里插入图片描述

从类结构图看,貌似它比ReentrantLock更复杂写,多两个内部类 ReadLockWriteLock,看着Lock提供的api完全一样,看来得从具体实现上来看其二者有什么样的差异了。

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

从ReentrantReadWriteLock的构造方法可以看出,它也支持公平锁和非公平锁,当然默认也是非公平锁。和ReentrantLock一样,加锁和解锁的实现逻辑都是在 Sync 里,所以我们重点看下Sync的实现,代码太多这里就不贴完整代码了,建议读者自行打开代码。
在这里插入图片描述

Sync

从Sync的类结构图来看,它还是相当复杂的,别急让我们来捋一捋,我们先从WriteLock看起(看起来会比较熟悉),看下他的lock和release的具体实现。

        @ReservedStackAccess
        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();   // 1
            int c = getState();    // 2
            if (c != 0) {    // 3 
                int w = exclusiveCount(c);   // 4
                if (w == 0 || current != getExclusiveOwnerThread())  // 5
                    return false;
                if (w == MAX_COUNT)   //6.  MAX_COUNT = 65535
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))  // 7
                return false;
            setExclusiveOwnerThread(current);  // 8
            return true;
        }    

如果你看过ReentrantLock的话,相信这段代码你已经完全能看懂了。这里我再大概说下这段代码的流程

  1. 获取到当前线程。
  2. 获取到锁对象的state值,state是保存了锁的状态。
  3. 如果state不为0,说明已经有线程加过锁了,这时候需要额外判断下,跳到4。 如果state为0,直接跳到 7。
  4. 获取到当前加写锁的次数,这里获取的是state的低16位。
  5. c已经不为0了,如果w不为0说明有线程加了写锁,如果加了写锁的线程也不是当前线程的,加锁就失败了。
  6. 这里需要额外判断下锁重入的次数,如果已经到65535就不能再加锁了,后续会解释为什么是65535。
  7. 执行CAS操作更改锁状态 state。
  8. 到这里说明加写锁已经成功了,把当前锁的持有者记录下来。
        @ReservedStackAccess
        final boolean tryReadLock() {
            Thread current = Thread.currentThread();   // 1
            for (;;) {
                int c = getState();  // 2
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)  // 3 
                    return false;
                int r = sharedCount(c);  // 4
                if (r == MAX_COUNT)   // 5
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {  // 6
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null ||
                            rh.tid != LockSupport.getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

读锁的加锁代码就完全不一样了,第一眼看到的不同就是这里有个大大的无限循环,我们还是来看下读锁的加锁过程。

  1. 获取当前线程。
  2. 获取锁的state状态值。
  3. 如果写锁的加锁次数不是0切写锁持有者不是当前线程,加读锁失败。
  4. 获取读锁的加锁次数,sharedCount(c)获取的是state的高16位。
  5. 如果读锁加锁次数达到65535,抛Error,和写锁一样,只能加65535次。
  6. 执行到这,说明可以加锁,使用CAS更新state成功后这里就开始记录一些读锁的状态信息,注意这里state增加值不是1,而是SHARED_UNIT(65536)。

看完readLock和writeLock的加锁方式就可以大体理解ReentrantReadWriteLock的实现了,原来它只是把ReentrantLock中的state分成两部分来用,高16位记录读锁状态,低16位记录写锁状态,如下图。
在这里插入图片描述
这也是为什么上文中加锁最大次数是65535的原因了,这也是而是SHARED_UNIT的值为65536的原因。

理解了加锁的代码,解锁部分也就好理解了,本质上是把加锁的代码反向执行下,代码如下。

        @ReservedStackAccess
        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;
        }

        @ReservedStackAccess
        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null ||
                    rh.tid != LockSupport.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;
            }
        }

Sync中还有一个ThreadLocalHoldCounter类,这个类的作用其实是记录每个线程对读锁的加锁测试,见名知意线程级的统计,代码也很简单,这里就不再贴了。
Sync中除了上文说到的几个加解锁的API,其余一些API就是获取Sync对象中各个状态的API,没什么好说的。

FairSync & NonfairSync

说完了抽象类Sync,我们来说下它的两个具体实现 FairSyncNonfairSync。 这两个实现类非常非常简单,只是重写了 writerShouldBlock()readerShouldBlock() 方法而已,如果你已经知道什么是公平和非公平了,这地方也就很好理解了。

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() { 
        // 写锁可以始终不被等待队列里的线程阻塞,只要当前锁是未锁定状态就可以加锁 
            return false;
        }
        final boolean readerShouldBlock() {  
        //这个方法判断队列的head.next是否正在等待写锁,这个方法确保读锁不应该让写锁始终等待,即便是非公平的,但写锁有更高的优先级,获取读锁还是得排队。
            return apparentlyFirstQueuedIsExclusive();
        }
    }

    // 公平锁就很好理解了,只要等待队列不为空,就得去排队  
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

ReadLock & WriteLock

在这里插入图片描述
在这里插入图片描述
其实看完Sync里的逻辑,基本上ReadLock和WriteLock的实现逻辑我们已经知道了。ReadLock和WriteLock只是向用户提供里有些功能抽象(实现了Lock中的方法),封装好了具体的实现,其实具体逻辑还是在Sync中实现。

从类继承关系来看,二者也只是简单

结论

了解完ReentrantReadWriteLock的实现后你就会发现,它其实和ReentrantLock一样,之前把ReentrantLock中的state切分成两部分用,高16位作为读锁的state,低16位作为写锁。如果把ReadLock和WriteLock拉出来单独看的话,二者都是一个ReentrantLock,只是不能像ReentrantLock那样重入那么多次而已。

ReentrantReadWriteLock的出现大幅提升了多读少写场景下的性能问题,但它依旧有自己的缺点,就是它可能会导致写饥饿。还是拿小区公告栏的例子,如果任意时刻都有人在看公告栏,你也不好打断人家所以你公告更新不了啊,所以想更新的人就得一直等着。
关注我,下次和大家一起看下 StampedLock 是如何解决饥饿问题的。

赞(0) 打赏
未经允许不得转载:XINDOO » ReentrantReadWriteLock源码解析

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫