ReentrantReadWriteLock源码解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 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 是如何解决饥饿问题的。

目录
相关文章
|
供应链 安全 Java
ReentrantLock源码解析
谈到多线程,就不避开锁(Lock),jdk中已经为我们提供了好几种锁的实现,已经足以满足我们大部分的需求了,今天我们就来看下最常用的ReentrantLock的实现。
62 0
|
7月前
|
算法 Java 容器
深入解析Java并发库(JUC)中的LongAdder
深入解析Java并发库(JUC)中的LongAdder
阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
|
Java API
ReentrantReadWriteLock源码解析
ReentrantReadWriteLock的出现大幅提升了多读少写场景下的性能问题,但它依旧有自己的缺点,就是它可能会导致写饥饿。还是拿小区公告栏的例子,如果任意时刻都有人在看公告栏,你也不好打断人家所以你公告更新不了啊,所以想更新的人就得一直等着。
51 0
|
程序员 Java 安全
【JUC基础】03. 几段代码看懂synchronized
程序员经常听到“并发锁”这个名词,而且实际项目中也确实避免不了要加锁。那么什么是锁?锁的是什么?今天文章从8个有意思的案例,彻底弄清这两个问题。
263 0
【JUC基础】03. 几段代码看懂synchronized
|
安全
AQS学习:ReentrantLock源码解析
AQS学习:ReentrantLock源码解析
53 0
|
Java uml
JUC系列学习(六):ReentrantReadWriteLock的使用及源码解析
`ReentrantReadWriteLock`是一种读写锁,跟`ReentrantLock`一样也是实现了`Lock`,区别在于`ReentrantLock`是独占锁,同一时刻只能有一个线程持有锁,`ReentrantLock`在某些场景下可能会有并发性能的问题。而**ReentrantReadWriteLock是独占锁(写锁)、共享锁(读锁)可以同时存在的一种读写锁,在读操作远大于写操作的场景中,能实现更好的并发性**。当读锁存在时,其他线程仍然可以获取读锁并进行读操作,但是不能获得写锁进行写操作;当写锁存在时,其他线程的读锁、写锁都是不允许的。
|
Java 开发者
JUC系列学习(三):ReentrantLock的使用、源码解析及与Synchronized的异同
`ReentrantLock`同`Synchronized`一样可以实现线程锁的功能,同样具有可重入性,除此之外还可以实现公平锁&非公平锁,其底层是基于`AQS`框架实现的。
ReentrantLock加锁源码解析
ReentrantLock加锁源码解析
99 0
ReentrantLock加锁源码解析
|
安全
AQS-AbstractQueuedSynchronizer源码解析(二)(上)
AQS-AbstractQueuedSynchronizer源码解析(二)
144 0
AQS-AbstractQueuedSynchronizer源码解析(二)(上)