Java精通并发-可重入读写锁底层源码分析及思想探究
前言:
在上一次咱们对于可重入的读写锁ReentrantReadWriteLock进行了初步的使用和了解,先来回忆一下上一次对于读写锁的使用代码:
这次则会聚焦在ReentrantReadWriteLock它对于AQS的基本使用上,因为本质上ReentrantReadWriteLock的底层依然是借助于AQS的力量来完成了读写锁相关的功能,从源码的结构也能够看出来:
读锁上锁的逻辑分析:
其实也就是分析这块的逻辑:
ReadWriteLock接口:
我们在使用时,代码是这么写的:
那看一下这个接口的定义:
它里面定义了一个读锁和写锁,而它有两个具体实现:
其中就有一个我们正在研究的ReentrantReadWriteLock。
ReentrantReadWriteLock()构造方法:
接下来看一下它的实例化的细节:
其中公平与非公平如想要指定,则可以手动调用带fair的构造既可,这个其实是跟我们之前https://www.cnblogs.com/webor2006/p/13629855.html所学习的ReentrantLock思想是一样的:
ReentrantReadWriteLock跟ReentrantLock在构造公平与非公平锁不同的是,多了两步:
这个也很好理解,因为是“读写”锁嘛,然后具体看一下这俩索的构建细节:
而这个Sync就是AQS一个实现,在上面开篇时也说明过:
所以,理解AQS的机制是何等的重要。
ReentrantReadWriteLock.readLock():
其实就是返回了一下实例:
ReentrantReadWriteLock.readLock().lock():
接下来核心就是来分析一下读锁上锁的逻辑了。
其中从“acquireShared”这个词就可以见名知义:读锁是共享锁,意味着多个“读”线程都可以获取到这把锁。其中注意传的参数是“1”。
AQS.acquireShared():
点击跟进去,发现就跳到AQS来了:
此时条件中会调用到tryAcquireShared()尝试进行锁的获取,点击进去瞅瞅:
里面未来实现,很明显此时的实现需要交由具体子类来,所以此时定位到具体子类中:
ReentrantReadWriteLock.Sync#tryAcquireShared():
接下来重点就回到具体的子类中瞅一下获取读锁的细节,定位到该方法,首先映入眼帘的是一段注释:
"Walkthrough",走查、攻略的意思,那很明显就是一个步骤说明,那,咱们根据这个官网的说明正好可以辅助逻辑的梳理:
1. If write lock held by another thread, fail.(如果此前有其它线程持有了一把写锁,直接就获取失败)
因为写锁是排它的,很明显如果有线程拿到写入的锁了,就禁止其它任何线程进行其它锁的获取了,很容易理解,那这个步骤对应的代码在这:
具体来看一下判断细节,先获取state的值,它是定义在AQS当中的:
接着看这个条件:
哇塞,居然写锁的线程数量是记住在state的低16位中的,而非直接用整个state这个变量来记录的,这块设计是非常精妙的,关于AQS的state这个变量,在不同的场景下其含义是不一样的,可以回忆https://www.cnblogs.com/webor2006/p/13549781.html,当时学习AQS就已经说明过:
为啥你知道在ReentrantReadWriteLock中AQS的state高16位是表示读锁线程的数量,低16位表示写锁线程的数量呢?代码上有官方说明呀,如下:
好,接下来,还有一个条件:
很明显它就是来判断持有写锁的不是当前线程,为啥还要加这个条件呢?因为ReentrantReadWriteLock中的写、读锁都是可重入的, 如果少了这个条件,那么对于当前线程有可能多次获取写锁,那么这种直接不应该直接返回失败的,这一点需要细细体会。
2. Otherwise, this thread is eligible for lock wrt state, so ask if it should block because of queue policy. If not, try to grant by CASing state and updating count. Note that step does not check for reentrant acquires, which is postponed to full version to avoid having to check hold count in the more typical non-reentrant case. (第二步:否则,该线程则有资格获取锁写状态,因此询问它是否因为队列策略而阻塞。如果没有,则尝试通过CASing状态和更新计数来授予。注意:这一步并不会检查可重入获取,它被推迟到完整版本以避免在更典型的非可重入情况下检查保持计数。)
a、条件判断:
说实话,这英语说明看得有点蒙蒙的,不管了,继续往下看代码,还是代码亲切点:
接下来又是一个条件判断:
一个个来看:
很明显这个就是对应官方的这块说明:
其实也就是判断是不是需要进行队列等待操作,具体的实现分为公平和非公平版本,大致瞅一下:
这里分别来看一下:
好,接着再来看第二个条件:
也就是需要判断读锁获取数量是否超过上限了,并不是读锁可以无限地可以进行获取,其MAX_COUNT的值是:
这个条件比较容易理解,接下来再看第三个条件:
从字面含义来看是比较并设置state,看一下方法说明:
其实也就是对应官方说明的这点:
b、条件通过,则更新读线程的状态:
简单说明一下,比如好理解:
3. If step 2 fails either because thread apparently not eligible or CAS fails or count saturated, chain to version with full retry loop.(如果第二步由于线程显然不符合条件或CAS失败或计数饱和而失败,那么会链接到具有完整重试循环的版本。)
上面英文说明又有些抽象,其实回到代码中就是执行到了它:
这里面的代码比较多,就不细看了,总之就是重试再次进行读锁的获取,这逻辑不看完全不影响咱们对于整体的流程的理解。
写锁上锁的逻辑分析:
其实也就是分析这块的逻辑:
ReentrantReadWriteLock.writeLock():
跟writeLock()一样,直接返回具体的实例:
ReentrantReadWriteLock.writeLock().lock():
接下来重点就是再来分析一下写锁的上锁逻辑了,由于已经有了上面读锁的上锁逻辑,这里直奔主题,代码就会走到这:
它又会调到AQS中的acquire()方法,注意读锁的获取调用的是AQS的acquireShared()方法,参数传的都是1:
这里又有一个攻略步骤说明,也来依据它来进行代码走读:
1. If read count nonzero or write count nonzero and owner is a different thread, fail(如果读取计数非0或写入计数非0且所有者是不同的线程,则失败)。
这个不看代码都比较好理解,因为是写锁,一定是排它互斥的,也就是在我写锁加锁之前,当前的这个对象既不能有读锁,也不能有写锁,所以,看一下代码:
注意这里有一个细节:
好,当上面两个条件都不满足,则会执行到这:
从注释就可以明白这代码的逻辑:可重入的获取, 也就是此时说明该线程之前已经获取到写锁了,再次获取又是它,根据可重入的原则,就只需要将state的值+1既可。
2. If count would saturate, fail. (This can only happen if count is already nonzero.)【如果计数会饱和,则失败。(这只会在count已经非零时发生。)】
关于它对应的代码其实就是它:
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.(否则,如果该线程是可重入获取或队列策略允许,则该线程有资格获得锁定。如果是,则更新状态并设置所有者。)
其实代码就走到这块了:
对于第一个条件,其实很好理解:
如果队列策略需要阻塞或者更改同步状态失败了【也就是有其它线程也在竞争写锁,而自己竞争失败了当然就代码写锁获取失败了】,都代表获取写锁失败,直接返回false了。
而如果这个条件不满足,那么:
读写锁上锁逻辑总结:
好,至此,咱们已经对于可重入的读写锁的上锁的主逻辑流程已经分析完了,接下来则重新总结一下;
读锁:
1、在获取读锁时,会尝试判断当前对象是否拥有了写锁,如果已经拥有,则直接失败;
2、如果没有写锁,就表示当前对象没有排他锁,则当前线程会尝试给对象加锁;
3、如果当前线程已经持有了该对象的锁,那么直接将读锁数量加1。
写锁:
1、在获取写锁时,会尝试判断当前对象是否拥有了锁(读锁与写锁),如果已经拥有且持有的线程并非当前线程,直接失败;
2、如果当前对象没有被加锁,那么写锁就会为当前对象上锁,并且将写锁的个数加1;
3、将当前对象的排他锁线程持有者设置为自己。
整体再来审视一下读写锁:
最后,再来整体挼一下ReentrantReadWriteLock的核心代码,首先它里面有三个核心成员:
而它之所以底层逻辑是建立在AQS基础之上的,是因为这个Sync:
而AQS中的state在这个场景下是被用作了两种状态:
这个是跟ReentrantLock的state完全不一样的,另外,值得提一点的是,对于之前https://www.cnblogs.com/webor2006/p/13168512.html咱们学习的ThreadLocal也在ReentrantReadWriteLock有应用到:
而对于Sync而言,在这里又分为公平和非公平的版本:
最后对于这个Sync是会被传到ReadLock和WriteLock中的:
而在我们调用lock()时,则会使用到这个sync:
这就是读写锁在底层实现上最为精华的部分, 而当底层拿不到锁的时候,依然是会进行阻塞的,而这个阻塞依然是要进行一个系统调用,此时又会涉及到之前https://www.cnblogs.com/webor2006/p/14060097.html咱们所提及的c++的packer类的park()和unpark()方法了,而这个系统调用其实也跟synchronized的是一样的,这也就是为啥synchronized底层c++层面的逻辑跟Lock在java层面的逻辑几乎是一样的,这个在未来的学习中会进一步体会到它们之间的关系。