本文代码基于Java8
前言
ReentrantReadWriteLock
是 Lock
的另一种实现方式, ReentrantLock
是一个排他锁,同一时间只允许一个线程访问,而 ReentrantReadWriteLock
允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。
读写锁内部维护了两个锁,一个用于读操作,一个用于写操作。所有 ReadWriteLock
实现都必须保证 writeLock
操作的内存同步效果也要保持与相关 readLock
的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。
ReentrantReadWriteLock
支持以下功能:
- 支持公平和非公平的获取锁的方式;
- 支持可重入。读线程在获取了读锁后还可以获取读锁,写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
- 允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;
- 读取锁和写入锁都支持锁获取期间的中断;
- 支持 Condition 。仅写入锁提供了一个 Conditon 实现,读取锁不支持 Conditon 。
readLock().newCondition()
会抛出UnsupportedOperationException
。
ReentrantReadWriteLock 类结构
1 | public class ReentrantReadWriteLock |
其中Sync
、FairSync
、NonfairSync
、ReadLock
、WriteLock
是 ReentrantReadWriteLock
的内部类,Sync
继承自 AbstractQueuedSynchronizer
,而 FairSync
、NonfairSync
继承 Sync
,分别对应公平锁和非公平锁。ThreadLocalHoldCounter
、HoldCounter
是 Sync
的内部类。
Sync 锁
Sync
也是一个继承于AQS的抽象类。Sync
也包括公平锁 FairSync
和非公平锁 NonfairSync
。sync 对象是 FairSync
和 NonfairSync
中的一个,默认是 NonfairSync
。
1 | abstract static class Sync extends AbstractQueuedSynchronizer { |
公平锁和非公平锁实现
和互斥锁 ReentrantLock
一样,读写锁也分为公平锁和非公平锁。公平锁和非公平锁的区别,体现在判断是否需要阻塞的函数是不同的。
1 | // 非公平锁 |
readerShouldBlock
的本质就是在检测这次获取读锁资源的操作时,AQS 的等待队列中是否已经有写锁了。
- 如果已经有写锁,那么要判断写锁是不是本线程,是本线程可以做锁降级。不是本线程就执行
fullTryAcquireShared
; - 如果没有写锁,就可以继续执行,做
r<MAX_COUNT
判断。
对公平锁而言,!readerShouldBlock()
就是 !hasQueuedPredecessors()
, h == t || ((s = h.next)!=null && s.thread == Thread.currentThread())
h==t
说明Node的等待队列为空(s = h.next)!=null && s.thread == Thread.currentThread()
说明等待队列中有值且是本线程申请锁资源。
满足以上2点的任何一个,可以申请读锁,继续执行下面的 r<MAX_COUNT
判断。
ReadLock 与 WriteLock
读锁
读锁是一个可重入的共享锁,获取读锁的思想(即 lock()
的步骤),是先通过 tryAcquireShared()
尝试获取共享锁。尝试成功的话,则直接返回;尝试失败的话,则通过 doAcquireShared()
不断的循环并尝试获取锁,若有需要,则阻塞等待。doAcquireShared()
在循环中每次尝试获取锁时,都是通过 tryAcquireShared()
来进行尝试的。
释放读锁的思想(即 unlock()
的步骤),是先通过 tryReleaseShared()
尝试释放共享锁。尝试成功的话,则通过doReleaseShared()
唤醒“其他等待获取共享锁的线程”,并返回true;否则的话,返回 flase。
1 | public static class ReadLock implements Lock, java.io.Serializable { |
写锁
写锁是一个可重入的排它锁。如果当前线程获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
1 | public static class WriteLock implements Lock, java.io.Serializable { |
使用示例
利用重入来执行升级缓存后的锁降级
锁降级指的是写锁降级成为读锁。锁降级是指把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。锁降级的意义在于:在一边读一边写的情况下感知数据变化并提高性能。
首先写锁是独占的,读锁是共享的,然后读写锁是线程间互斥的,锁降级的前提是所有线程都希望对数据变化敏感,但是因为写锁只有一个,所以会发生降级。如果先释放写锁,再获取读锁,可能在获取之前,会有其他线程获取到写锁,阻塞读锁的获取,就无法感知数据变化了。所以需要先hold住写锁,保证数据无变化,获取读锁,然后再释放写锁。
如果长时间用写锁独占,对于某些高响应的应用是不允许的。
1 | class CachedData { |
使用 ReentrantReadWriteLock 来提高 Collection 的并发性
通常在 collection 数据很多,读线程访问多于写线程并且附带操作的开销高于同步开销时尝试这么做。
1 | public class RWDictionary { |
总结
相对于排他锁,读写锁提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时 ReentrantReadWriteLock
能够提供比排他锁更好的并发性和吞吐量。