ReentrantReadWriteLock
读写锁ReentrantReadWriteLock下,一个资源可以被多个读线程访问或一个写线程访问,但不能同时存在读写线程,也就是读读共享,读写互斥。多线程并发可以访问大面积的可以容许多个线程来读取,相对于ReentrantLock来说,ReentrantReadWriteLock更适用于读多写少的场景,但在写操作的时候二者性能是相差不多的,在ReentrantReadWriteLock中,可以一块使用读锁和写锁,但是需要先获取写锁,获取读锁,再释放写锁,释放读锁,通过锁降级来实现,但如果顺序颠倒,就会导致死锁。
从无锁到互斥同步锁(Synchronized和ReentrantLock),然后就是ReadWriteLock接口下的ReentrantReadWriteLock,他们从安全性和并发性考虑,无锁在多线程访问情况下,安全性是无法保证的,而独占式的互斥同步锁ReentrantLock和Synchronized,虽然对比无锁访问程序从无序到有序,数据具有一致性。
ReentrantReadWriteLock的特性
公平性选择
支持公平锁与非公平锁的获取方式,但是比较吞吐量非公平锁还是优于公平锁。
该特性与ReentrantLock相似,在默认情况下是获取非公平锁,构造方法传入true获取公平锁。
可重入
支持可重入,以读写线程为例,读线程在获取了读锁之后,能够再次获取读锁,写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁(锁降级)。但是写线程不能先获取读锁,再获取写锁,这样会让线程陷入死锁。
ReentranReadWriteLock中使用一个int state变量来维护读锁和写锁的状态。int类型为4字节32位,用state的高16位来存储读锁的状态,用低16位来存储写锁的状态。主要是为了实现可重入,因为读锁使用高16位来记录,因此每次获取读锁成功都需要+1,再向左移动16位。
锁降级
锁降级的流程:
获取写锁--->获取读锁--->释放写锁--->释放读锁
重入还允许通过获取写入锁定,然后读取锁,再释放写锁,从写锁到读取锁,但是从读锁升级到写锁是不可能的,其实可以参考文件权限,写文件的权限是大于读文件的权限,这样的锁降级的设计其实是为了写完可以马上读,防止其他线程对数据进行修改,目的是为了保证数据的可见性,其实是变为了悲观锁。
Oracle公司在ReentrantReadWriteLock底层源码中有一个缓存数据类,volatile 修饰的cacheValid变量,保证了数据的可见性,在先获取读锁时,如果cache是不可用的,那么就释放读锁,获取写锁,修改数据之前,要先检查cacheValid,然后修改数据,将cacheValid为true,在释放写锁前抢占读锁,此时cache中的数据可以使用,释放写锁,释放读锁。
StampedLock(邮戳锁)
StampedLock解决了ReentrantReadWriteLock可能会产生写锁饥饿问题,StampedLock是一个乐观锁,stamp是戳记的意思,代表了锁的状态,当stamp返回0时,表示线程获取锁失败,当释放锁/转换锁时,都要传入最初获取的stamp值。并且StampLock是不可重入的。
获取锁时返回一个邮戳,stamp为0失败,其余为成功。
释放锁时,需要一个邮戳,stamp必须是和成功获取锁时得到的stamp一致。
访问模式 | 描述 |
Reading(读模式) | 与ReentrantReadWriteLock读锁类似 |
Writing(写模式) | 与ReentrantReadWriteLock写锁类似 |
Optimistic Reading(乐观读模式) | 无锁机制,支持读写并发,假如检查到邮戳被修改,那么升级为悲观锁再读一次。 |
乐观读中有写操作插入,如果戳记没有被修改,就依旧是乐观锁,但戳记被修改,与初始的戳记对比不相同,就返回false,需要将乐观读变为悲观读,也就是需要读写互斥,得到新的修改值。
StampedLock缺点
- 不可以重入(从一个锁到另一个锁)
- StampedLock的悲观读锁和写锁都不支持条件变量(Condition)
- 使用StampedLock不要调用中断操作