ReentrantLock、ReentrantReadWriteLock、StampedLock

2022-07-20 12:27:41

ReentrantReadWriteLock

读写锁定义为:
一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
ReentrantReadWriteLock 并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的,
大多实际场景是 读读线程间并不存在互斥关系,只有读、写线程 或者是写写线程间的操作需要互斥的。

一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁(切菜还是拍蒜选一个)。
也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。

只有在读多写少情境之下,读写锁才具有较高的性能体现。

锁的严苛程度变强叫做升级,反之叫做降级
在这里插入图片描述
锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)

锁降级:遵循获取写锁----->再获取读锁------->再释放写锁的次序,写锁能够降级成为读锁
如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁

在这里插入图片描述

重入还允许通过获取写入锁定,然后读取锁然后释放写锁,从写锁到读取锁,但是,从读锁定升级到写锁是不可能的。

如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。
所以,需要释放所有读锁,才可获取写锁

在这里插入图片描述
写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁)这是因为写锁要保持写操作的可见性。

因为,如果允许读锁在被获取的情况下写锁的获取,那么正在运行的其他线程无法感知到当前写线程的操作。

因此,分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:
读锁全完,写锁有望;写锁独占,读写全堵

如果有线程正在读,写线程需要等待读线程释放后才能获取写锁
即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,
也就是写锁必须等待,这是一种悲观的读锁,人家还在读着那,你就先别写,省的数据乱。

ReentrantWriteReadLock支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。
在这里插入图片描述
1 代码中声明了一个volatile类型的cacheValid变量,保证其可见性。

2 首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性。

如果违背锁降级的步骤
如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。

如果遵循锁降级的步骤
线程C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的。

邮戳锁StampedLock

StampedLock是JDK1.8中新增的一个读写锁,
也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化, 也叫票据锁
stamp(戳记,long类型): 代表了锁的状态,当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。

锁饥饿问题:
ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,
假如当前100个线程,99个读,1个写,有可能99个读取线程长时间抢到了锁,那1个写线程就悲剧了,因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。

如何解决锁饥饿?
1.使用公平策略可以一定程度上缓解这个问题,new ReentrantReadWriteLock(true);
但是公平策略是以牺牲系统吞吐量为代价的,
ReentrantReadWriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取写锁的时候,其他写操作和读操作都会处于阻塞状态,
读锁和写锁也是互斥的,所以在读的时候不允许写的,读写锁比传统的synchronized速度要快很多,
原因就是在于ReentrantReadWriteLock支持读并发

stampedLock横空出世:
reentrantReadWriteLock 的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采用乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以在获取乐观读锁后,还需要对结果进行校验。

StampedLock的特点:

所有获取锁的方法,都会返回一个邮戳stamp,stamp为表示获取失败,其余都标识成功。
所有释放锁的方法,都需要一个邮戳Stamp,这个Stamp必须是成功获取锁得到的stamp一致
StampedLock是不可重入的,(如果一个线程已经持有写锁,再去获取写锁的话就会造成死锁)

stampedLock有三种访问模式:
1 Reading 读模式: 功能和reentrantReadWriteLock读锁类似
2.Writing 写模式:功能和ReentrantReadWriteLock的写锁类似
3.Optimistic reading 乐观读模式: 无锁机制,类似于数据库中的乐观锁,支持读写并发,
很乐观的认为读取时没人修改,加入被修改再实现升级为悲观读模式

StampedLock的缺点:

1.StampedLock不支持重入,没有Re开头
2.StampedLock的悲观读锁和写锁都不支持条件变量(Condition)
3.使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法
如果需要支持中断功能,一定使用可中断的悲观读锁, readLockInterruptibly()和写锁writeLockInterruptibly()

  • 作者:散_步
  • 原文链接:https://blog.csdn.net/zhumengguang/article/details/115441774
    更新时间:2022-07-20 12:27:41