上一篇文章我们介绍了一个显式锁,ReentrantLock ,了解到它是一个『独占式』锁,简而言之就是,
我拿到锁以后,不管我是读或是写操作,其他人都不能和我抢,都得等着。
因而在某些读操作远大于写操作的场景之下,即便我只是读数据也不得不排队一个一个来,于是有人提出了一个『读写锁』的概念。
『读写锁』并不是真正意义上的读写分离,它只允许读读共存,而读写、写写依然是互斥的,所以只有在大量读操作、少量甚至没有写操作的情境之下,读写锁才具有较高的性能体现。
来自父接口的规范
ReentrantReadWriteLock 继承了接口 ReadWriteLock,而父接口约束它必须提供的能力如下:
而 ReentrantReadWriteLock 对该接口的实现也是简单明了了的:
显然,ReentrantReadWriteLock 通过在内部定义两个静态内部类来分别实现接口 Lock,以达到内嵌读写锁的能力,而两个内部类的实现是如何的?区别在哪?怎么实现一个读一个写?我们稍后会详细地从源码层面一点点分析,不要着急。
自定义实现 AQS
AQS 是什么呢?相信看过之前文章的朋友是一定知道的,AQS 指的是 AbstractQueuedSynchronizer,就是一个同步容器。简而言之就是:
一个队列、一个状态、一个线程对象。
线程对象保存的当前被允许访问代码块的线程实例,队列中每一个线程都是一个节点,这些线程都是由于没能获取到锁而阻塞排队在这里。状态可以取值为零或正正整数,零表示当前无人持有该锁,正数表示当前线程多次重入该锁的次数。
除此之外,ReentrantReadWriteLock 中剩余的一些方法主要提供了该锁的一些状态信息的返回,这部分比较简单。本文的重点将放在对那两个内部类实现的读锁写锁原理的分析。
下面我们深入到源码层面去看看读锁在何种情况下才能成功的加在临界资源上,哪些情况下不得加读锁。另外说一句,对于有些方法我并不会一跟到底,不然篇幅太长了,我会大体概括这些方法的作用与核心逻辑,具体的大家可以自行阅读分析。
ReadLock 是 ReentrantReadWriteLock 中定义的一个内部类,它实现了 Lock 接口,提供基本的 lock、unlock 等方法,我们先看 lock 方法:
public void lock() {
sync.acquireShared(1);
}
lock 方法很简单,调用了外部类同步容器实例的同步方法,因为需要读写分离,所以读锁写锁必须共用同一个 AQS,而这个 AQS 则定义在外围类 ReentrantReadWriteLock 之中,供两种锁使用。
简而言之,无论是读锁或是写锁,他们共用的一个 AQS 同步器,同一个阻塞队列,同一个状态,同一个线程持有器。ReentrantReadWriteLock 也正是通过这个公用的 AQS 同步器来协调读锁写锁能同时工作。
acquireShared 方法实现如下,这里我们先以公平策略作引例:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared 方法实现如下:
这个方法的代码不铺开分析了,主要切三个部分总结下逻辑及完成的功能,具体源码大家自行分析了,如有疑问欢迎加我微信一起探讨(文末)。
分析完了 tryAcquireShared 方法以后,我们知道如果此次尝试加锁失败,方法会返回值 -1,意味着加读锁失败,当前线程需要被阻塞排队等待。
于是就有了我们的 doAcquireShared 方法,该方法会将当前线程包装成节点添加到阻塞队列尾部,排队等待再次竞争临界资源。
这样的话,我们关于读锁的加锁大致上也摸清楚了,总结一下整个过程:
首先 lock 方法会调用 tryAcquireShared 方法做一次尝试加锁操作,如果成功了那么整个加锁的过程也就结束了,否则还会去区分是什么原因导致的失败。
如果是由于临界资源正在被写锁锁住,那么认为你不应该再尝试了,先去阻塞等着吧,而如果是由于并发修改 state 导致的失败,那么将进入循环尝试,直到成功或是遇到和上述一样的情况,有写锁成功的占有了临界资源,不得继续尝试。
tryAcquireShared 失败后将导致 doAcquireShared 方法的调用,将当前线程包装成节点添加到队列的尾部,然后阻塞在循环体之中,等待别人的唤醒。
接下来我们来看看读锁的 unlock 方法实现:
类似的代码结构,我们看 tryReleaseShared 方法:
两个部分,比较简单:
只有当所有的读锁都释放结束之后,该方法才会返回 true 并转而去执行方法 doReleaseShared 试图唤醒队列中下一个状态正常线程。
关于第二步,很多人可能根本不知道为啥这么做,这里简单说一下:
我们的 doAcquireShared 方法尝试阻塞当前线程的过程中有这么一个过程,就是在实际阻塞之前会判断一下当前线程节点是不是排在队列的第一个,如果是则作最后一次尝试,一旦失败就真正阻塞了,成功的话会调用 setHeadAndPropagate 方法。
这个方法会将当前节点置换到 head 节点上,并且调用 doReleaseShared 将自己的 waitStatus 值改成 PROPAGATE,象征一种「传播」特性,并且队列此时没有人在排队,所以下一个读锁会无条件的成功,就这样一直传播下去,直到任一线程失败了才将头结点的传播状态修改为 SINGLE,以此释放 doReleaseShared 的释放能力。
说一下哈,有关「传播特性」,市面上分析这部分源码的文章大多都选择略过或是含糊其辞,没怎么搜索到描述详尽的资料。以上是我个人理解,各位要是有疑问,也欢迎大家和我一起交流探讨!
关于读锁的释放,我想我已经描述的很清晰了,总结下大体逻辑:
每调用一次 tryReleaseShared 都会减少一次读锁的持有数量,只有读锁的持有量为零,该方法才会返回 true,并接着调用 doReleaseShared 方法释放队列中第一个有效的阻塞节点,让它重新竞争临界资源添加读锁,这个过程本来是很简单的,就节点向前移动并唤醒线程而已,但是其中涉及了一个「传播」共享传递,需要额外去理解,这一点我们上述也做了说明了。
分析完了读锁的加锁和释放锁的过程,接下来我们分析写锁的添加和释放过程是如何彼此互斥工作的。
写锁的 lock 方法调用 acquire 方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
和读锁的尝试加锁方法具有相似的代码风格,都是先通过一个 tryXXX 方法尝试加锁,失败了就会返回调用另一个方法阻塞当前线程到等待队列上。我们先看这个 tryAcquire 方法:
如果你认真的分析了读锁的源码,你会发现写锁的尝试加锁就非常简单了。
写的分析文字有点多,但是这个尝试加锁的代码逻辑确实是简单易理解的。
我们再回到 acquire 方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如果 tryAcquire 失败了,那么将调用 acquireQueued 添加当前线程到等待队列上并阻塞当前线程,我们一起看看这个方法的实现:
这个方法也是不难的,两个部分,前一个部分是做「临死挣扎」,如果自己是队列首个有效的线程节点,那么将再进行一次尝试,如果成功即刻返回而不必阻塞自己,否则将通过调用 LockSupport 中的 unpark 方法阻塞当前线程。
接着我们看写锁的释放实现逻辑:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
废话不多说,直接看 tryRelease 方法:
如果 state 中代表写锁持有数量值减去一还不等于零,那么说明当前线程多次重入该写锁,于是修改 state 的值,让写锁持有数量减一,返回 false。
否则,认为该线程的多次重入已经全部退出了,这时才会返回 true,表示写锁全部释放。
这时我们回到 release 方法,剩余的代码也已经明了了,如果返回了 true,也即写锁全部释放了,那么将唤醒队列中等待着的第一个有效结点线程,唤醒之后方法返回 true,表示写锁释放完成,否则返回 false,表示写锁释放失败,多次的重入并没有得到完全的释放。
总的来说,写锁的加锁与释放相对于读锁来说是简单的,因为它是互斥了,没那么多条件,不管你是什么锁,只要你正在占用临界资源,那么我就等待。而相对于读锁来说,它需要去区分读线程正在使用资源、还是写线程线程正在使用资源。
所以,读写锁的复杂点在于读锁的共存,写锁是互斥的,没有过多的要求,重点在于对读锁的理解。
<center>关注公众不迷路,一个爱分享的程序员。</center>
<center>公众号回复「1024」加作者微信一起探讨学习!</center>
<center>每篇文章用到的所有案例代码素材都会上传我个人 github</center>
<center>https://github.com/SingleYam/overview_java</center>
<center>欢迎来踩!</center>
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。