ReentrantReadWriteLock简单分析

6月 16th, 2016 1,939 留下评论 阅读评论

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成功或者返回失败

  1. 判断是否被写锁占用且当前线程不是写锁拥有者,return -1
  2. 判断读锁重进入总数(所有线程)是否超过最大值
  3. 根据公平性原则(Sync子类的readerShouldBlock()方法),判断是否可以加读锁
  4. 然后就尝试CAS加锁,若成功则获取当前线程的HoldCounter对象,更新count++(更新缓存hc对象),return 1
  5. CAS miss,call fullTryAcquireShared()
  6. fullTryAcquireShared 循环开始(此方法阻塞
  7. 判断是否被写锁占用且当前线程不是写锁拥有者,return -1
  8. 再判断(rh.count | w) == 0 && readerShouldBlock(current),(rh.count | w) == 0就是rh.count == 0 || w == 0(当前线程未占有读锁或未占用写锁,即除了占有写锁后的读锁重进入的情况),都要根据公平性原则(Sync子类的readerShouldBlock()方法),判断是否可以加读锁
  9. 判断读锁重入是否超过最大值
  10. 尝试CAS加锁,若成功更新rh.count++,return 1
  11. 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

  1. 添加节点到AQS的Sync队列中
  2. 无限循环开始
  3. 判断当前节点若是first节点,到步骤4,若不是到步骤6
  4. call tryAcquireShared,若加锁成功到步骤5,若失败到步骤6
  5. call setHeadAndPropagate,更新Sync队列的head节点,并在一定条件下调用AQS.doReleaseShared,并跳出循环返回
  6. 判断当前节点线程是否需要park,并执行线程park
  7. 回到循环开始,步骤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()方法,即失去了公平性保证也抛弃了写优先于读的策略。

Categories: Java 标签:, ,
  1. 还没有评论呢。