ReentrantReadWriteLock简单分析
AQS的另一个实现,可重入的读写锁,其中读锁是共享锁,写锁是排他锁。用法和ReentrantLock基本一致。
ReentrantLock的源码分析见:可重入锁、ReentrantLock、AQS、Condition,作为本文的铺垫。
ReentrantLock利用AQS的state值作为重入次数记录,所以理论上可以重入int的最大值;ReentrantReadWriteLock同样利用AQS的state值来作为重入次数记录,只不过32位的int值,前16位用来记录读锁的重入次数,后16位用来记录写锁的重入次数,所以读锁和写锁的可重入次数最多都只有65535次。
ReentrantReadWriteLock的代码结构和ReentrantLock基本一致,先有一个Sync类继承自AQS实现了全部的写锁和读锁的加锁、解锁代码,然后再有UnfairSync和FairSync继承自Sync来实现非公平和公平策略,两个子类中只实现了readerShouldBlock()和writerShouldBlock()。不同的是,还有两个新的类,ReadLock和WriteLock,内部持有Sync实例,ReadLock的lock()和unlock()调用acquireShared()和releaseShared(),WriteLock的lock()和unlock()调用acquire()和release(),都只是对Sync实例方法的直接调用,没啥可看的东西。
Sync类
state值拆分
一开始的SHARED_SHIFT、SHARED_UNIT、MAX_COUNT、EXCLUSIVE_MASK变量就是对state值进行前16位和后16的拆分,还有后面sharedCount()和exclusiveCount()方法。
针对每个线程的读锁的HoldCounter
state值的前16位,记录了所有线程的读锁的重入次数,所以每个线程中还要自己单独在记录一下读锁重入次数(才能做到读锁的重入和释放)。这个数据就保存在HoldCounter
类中,并用ThreadLocalHoldCounter extends ThreadLocal<HoldCounter>
类来存储。
HoldCounter cachedHoldCounter这个变量可以无视,缓存上一次进入读锁的HoldCounter对象,没有这个功能也没太大影响每次都从ThreadLocal中取就可以了,只是提高了效率。
Sync类的子类,UnfairSync和FairSync
都只实现了writerShouldBlock()和readerShouldBlock()方法。
FairSync简单,所有试图acquire锁都必须是Sync队列中的first才行,保证公平性。
UnfairSync的write操作,没有任何需要Block的场景,而read需要判断Sync队列的first是否是写(独占)请求,表明了写的优先级比较高,读在很多情况下要给写让步,防止写锁饥饿。
代码实现
写锁的实现
调用AQS.acquire和AQS.release实现,然后通过Sync.tryAcquire()和Sync.tryRelease()实现,和ReentrantLock基本没有差别。在tryAcquire中既要判断state的互斥部分(是否有写锁占用),也要判断state的共享部分(是否有读锁占用)。
读锁的加锁实现
调用AQS.acquireShared和AQS.releaseShared实现,然后通过Sync.tryAcquireShared()和Sync.tryReleaseShared()实现。
Sync.tryAcquireShared(此方法阻塞至acquire成功或者返回失败)
- 判断是否被写锁占用且当前线程不是写锁拥有者,return -1
- 判断读锁重进入总数(所有线程)是否超过最大值
- 根据公平性原则(Sync子类的readerShouldBlock()方法),判断是否可以加读锁
- 然后就尝试CAS加锁,若成功则获取当前线程的HoldCounter对象,更新count++(更新缓存hc对象),return 1
- CAS miss,call fullTryAcquireShared()
- fullTryAcquireShared 循环开始(此方法阻塞)
- 判断是否被写锁占用且当前线程不是写锁拥有者,return -1
- 再判断(rh.count | w) == 0 && readerShouldBlock(current),(rh.count | w) == 0就是rh.count == 0 || w == 0(当前线程未占有读锁或未占用写锁,即除了占有写锁后的读锁重进入的情况),都要根据公平性原则(Sync子类的readerShouldBlock()方法),判断是否可以加读锁
- 判断读锁重入是否超过最大值
- 尝试CAS加锁,若成功更新rh.count++,return 1
- CAS miss,回到循环开始,步骤6
官方注释中说,fullTryAcquireShared方法handles CAS misses and reentrant reads not dealt with in tryAcquireShared,其中CAS miss是很好理解的,但是reentrant reads还真没看出来(读重入完全有可能在tryAcquireShared的CAS里就完成了呀,可能作者的意思是读重入因为并发比较高所以容易CAS miss?)。
AQS.acquireShared
在执行完Sync.tryAcquireShared失败时,会call AQS.doAcquireShared,将线程添加到AQS的Sync队列中,并循环等待加锁成功,类似独占锁执行的AQS.acquireQueued方法。
AQS.doAcquireShared
- 添加节点到AQS的Sync队列中
- 无限循环开始
- 判断当前节点若是first节点,到步骤4,若不是到步骤6
- call tryAcquireShared,若加锁成功到步骤5,若失败到步骤6
- call setHeadAndPropagate,更新Sync队列的head节点,并在一定条件下调用AQS.doReleaseShared,并跳出循环返回
- 判断当前节点线程是否需要park,并执行线程park
- 回到循环开始,步骤2
doAcquireShared和acquireQueued的区别在于,加锁成功后,acquireQueued仅仅就是更新了head节点,而doAcquireShared不仅更新了head节点,还会在一定条件下call doReleaseShared方法去激活下一个等待的节点。
AQS.setHeadAndPropagate和AQS.doReleaseShared
这一点可以理解为,因为doAcquireShared是获取共享锁,获取共享锁的前提是没有独占锁被持有,而共享锁被一方获取后,其他方可以同时去尝试获取共享锁,所以可以来激活下一个等待的是共享模式的节点线程,提高效率。这样子可以把Sync队列中依次顺序的共享锁请求全部打开并满足,提高了并发。
所以这里也可以看出,当一个线程获得写锁后,并不是所有等待写锁的线程都会被激活并获得共享写锁,因为Sync队列中是按照公平原则来的,只要队列中有一个独占锁请求(写锁),后面的共享锁(读锁)请求是依然被阻塞的,按照时间顺序保证了这些读锁请求会在写锁之后执行,读到新的数据。
读锁的解锁实现
解锁的简单了,调用Sync.tryReleaseShared(),解锁成功后调用AQS.doReleaseShared(),激活下面节点的线程。
Sync.tryReleaseShared(此方法阻塞至release成功,返回是否锁被释放)
读锁与写锁的实现区别
读锁(共享锁)的加锁的阻塞,在Sync.fullTryAcquireShared中也有实现(在Sync中出现acquire失败后才进入AQS里的阻塞实现),而在Sync中实现acquire成功,是不会调用AQS.doReleaseShared去对下面节点进行线程唤醒的(这个就有点不明白了)。
ps:读锁(共享锁)的加锁这一块是最复杂的,理解也还不甚到位。
读锁(共享锁)的解锁也需要在阻塞,也在Sync中实现,因为共享锁在解锁时也存在并发;而写锁(独占锁)是不需要阻塞实现的,因为解锁的时刻肯定是只有当前线程可以操作锁对象,不存在并发。
tryReadLock和tryWriteLock
这两个方法acquire的逻辑和普通的lock没有区别,除了少判断了writerShouldBlock()和readerShouldBlock()方法,即失去了公平性保证也抛弃了写优先于读的策略。