之前提到的锁(如 Metux 和 ReentrantLock )基本都是排他锁,这些锁在同一时刻只允许一个线程访问,而读写锁(ReentrantReadWriteLock)在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和写线程都会被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。多个读锁不互斥,读锁和写锁互斥,这是由 JVM 自己控制的,你只要上好锁就可以了。
读写锁的性能都会比排他锁好,因为在大多数场景下读多于写,在读多于写的情况下,读写锁比排他锁有更好的并发性和吞吐量,并发包 读写锁的实现类是 ReentrantReadWriteLock,它的特性如下:
1)公平性:支持非公平(默认方式)和公平的锁获取方式,吞吐量还是非公平优于公平
2)重入性:该锁支持重入,以读写线程为例:读线程获取了读锁之后,能够再次获取读锁。而写线程获取了写锁之后能够再次获取写锁,同时也可以获取读锁(锁降级)
3)锁降级:遵循获取写锁、获取读锁再释放写锁的顺序,写锁能够降级为读锁
4)ReadLock 可以被多个线程持有并且在持有时排斥任何的 WriteLock,而 WriteLock 则是完全的互斥。这一特性非常重要,对于高频率读取而相对低频率写入的场景,使用此锁可以提高并发量
5)不管是 ReadLock 还是 WriteLock 都支持 interrupt,语义和 ReentrantLock 一致
6)WriteLock 支持 Condition 并且与 ReentrantLock 语义一致,而 ReadLock 不能使用 Condition,否则抛出 UnSupportedOperationException 异常
(2)没有写请求或者有写请求(调用线程和持有锁的线程是同一个)
读写锁 ReadWriteLock 接口仅仅定义了 readLock 和 writeLock 两个方法,而其实现类 ReentrantReadWriteLock 除了实现两个接口方法外,还提供了一些便于外界监控其内部工作状态的方法,这些方法如下:
下面我们使用读写锁实现一个非线程安全的 HashMap 的缓存,代码如下:
接下来我们分析 ReentrantReadWriteLock 的实现,主要包括:读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级。
ReentrantReadWriteLock 同样依赖于自定义同步器实现同步功能,而读写状态就是其同步器的同步状态。回想 ReentrantLock 中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态,就一定要“按位切割使用”这个变量,读写锁将变量切分成两个部分,高16位表示读,低16位表示写,划分方式如下图所示:
写状态: S & 0X0000FFFF(将高16位全部抹去,即 左边16个0,右边16个1),写状态增加 1 时,等于 S +1
读状态: S >> 16(无符号补0,右移16位,只剩下高位16位),读状态增加 1 时,等于 S +(1 <<16),也就是 S + 0X00010000
根据状态的划分得出一个推论:S 不等于 0 时,当写状态(S & 0X0000FFFF)等于0,而读状态(S >> 16)不等于 0 时,此时是读锁已被获取