读写锁用的是同一个Sycn同步器,因此等待队列,state等也是使用同一个
下面使用一个案例带大家了解以下读写锁的原理:
1.t1线程获取写锁
t1成功上锁,流程与ReentrantLock加锁相比没有特殊之处,不同的是写锁使用state的低16位,而读锁占了state的高16位
以下是写锁的tryAcquired方法源码:
2.t2线程获取读锁
t2执行r.lock,这时进入读锁的sync.acquiredShared(1)流程,首先会进入tryAcquiredShared流程。如果有写锁占据,且当前线程不是获取写锁线程,那么返回-1表示失败
tryAcquiredShared返回值表示
- -1表示失败
- 0表示成功,但是后继节点不会继续被唤醒
- 正数表示成功,而且数值是还有几个后继节点需要被唤醒,读写锁返回1
tryAcquiredShared方法代码如下:
然后会进入sync.doAcquireShared(1)流程,首先也是调用了addWait添加节点,也是添加两个节点,第一个节点为哨兵节点(不同的是节点被设置为Node.SHARED模式而非Node.EXCLUSIVE模式,注意此时t2仍处于活跃状态)
4.然后t2会获取前驱节点,如果前驱节点是头节点就说明t2是老二节点,还会再次调用tryAcquireShared(1)尝试获取锁
5.如果获取成功,在doAcquireShared内for(;;)循环,第一次在调用shouldParkAfterFailedAcquired(p,node)方法时会将前驱节点的waitStatus设置为-1返回false,就会再次循环尝试tryAcquireShared(1)获取锁如果还不成功,这时调用shouldParkAfterFailedAcquired(p,node)方法发现前驱节点的waitStatus设置为-1将返回ture,就会执行后续的parkAndCheckInterrupt()进行park阻塞
doAcquiredShared方法代码如下:
6.这时又有t3线程过来加读锁,t4线程过来加写锁,就会变成下面局势:
7.写锁的unlok流程:
这时会走到写锁的sync.release(1)流程,调用sync,tryRelease(1)成功,变成下面的样子
release代码如下:
tryRelease代码如下:
8.接下来会去执行唤醒流程unparkSuccessor,就是让阻塞的老二节点恢复运行,这时t2在doAcquireShared内parkAndCheckInterrupt阻塞处恢复运行,返回再来一次for循环,执行tryAcquiredShared成功则让读锁计数加一
doAcquireShared代码如下:
9.这时t2线程已经恢复运行了,t2会调用setHeadAndPropagate(node,1)方法,它原本所在节点会被设置为头节点。
10.然后再setHeadAndPropagate方法里还会检查下一个节点是否是shared共享节点,如果是则调用doReleaseShared()将head的状态从-1改为0并且唤醒老二(防止在改动的过程中其他线程来进行干扰,避免重复唤醒),这时t3在doAcquireShared里parkAndCheckInterrupt()处恢复运行(这就是读读并发的原理)
setHeadAndPropagate方法代码:
doReleaseShare方法代码:
这时t3已经恢复运行,接下来t3调用setHeadAndPropagate方法将原有节点设置为头节点
11.t2 r.unlock,t3 r.unlock
t2进入sync.releaseShared(1)中,调用tryReleaseShared(1)让计数减一,但是由于计数还不为零,所以不执行doReleaseShared方法
t3进入sync.tryReleaseShared(1)中,调用tryReleaseShared(1)让计数减一,这时计数为零了,进入doReleaseShared方法将头节点的waitStatus从-1改为0,然后唤醒第二节点
之后t4在acquireQueued中parkAndCheckInterrupt处恢复运行,再次for(;;)这次自己是老二线程,并且没有其他竞争,tryAcquired(1)成功,修改头节点,流程结束
acquireQueued方法代码如下:
学习笔记——资料from黑马程序员