无锁 => 独占锁 => 读写锁 => 邮戳锁
ReentrantLock, ReentrantReadWriteLock, StampedLock
4.12.1 面试题
- Java有哪些锁?
- 对于读写锁,锁饥饿问题是什么?
- 有没有比读写锁更快的锁?邮戳锁
- StampedLock知道码?(邮戳锁/票据锁)
- ReentrantReadWriteLock的锁降级机制是什么?
4.12.2 ReentrantReadWriteLock
读写锁的定义:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
class MyResource{Map<String,String> map = new HashMap<>();// Lock lock = new ReentrantLock();ReadWriteLock readWriteLock = new ReentrantReadWriteLock();public void write(String key, String value) throws InterruptedException {// lock.lock();readWriteLock.writeLock().lock();try {System.out.println("正在写入");map.put(key, value);TimeUnit.MILLISECONDS.sleep(50);System.out.println("完成写入");}finally {//lock.unlock();readWriteLock.writeLock().unlock();}}public void read(String key) throws InterruptedException {//lock.lock();readWriteLock.readLock().lock();try {System.out.println("正在读取");map.get(key);TimeUnit.MILLISECONDS.sleep(5000);System.out.println("完成读取");}finally {//lock.unlock();readWriteLock.readLock().unlock();}}
ReentrantReadWriteLock锁降级:将写锁降级为读锁,反之叫锁升级
- 一个线程锁降级流程
- 获取
写
锁 - 获取
读
锁 - 释放
写
锁
- 获取
如果先获取读锁,并且没有释放时,线程是无法获取写锁的。因此ReentrantReadWriteLock无法完成锁升级
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();readWriteLock.writeLock().lock(); // 1. 获取写锁
// 写锁状态
readWriteLock.readLock().lock(); // 2. 获取读锁
readWriteLock.writeLock().unlock();// 3. 释放写锁// 锁降级之后为读锁状态readWriteLock.readLock().unlock();
统一线程先获取写锁再去获取读锁,相当于重入。
4.12.3 StampedLock
- 定义
- StampedLock是JDK1.8新增的一个读写锁,是对ReentrantReadWriteLock的优化。
- 邮戳锁, 也成为票据锁
- stamp(戳记,long类型):代表了锁的状态。当stamp返回0时,表示线程获取锁失败,并且,当释放锁或者转换锁时,都要传入最初获取的stamp值。
- 作用
- 解决锁饥饿:对于短的只读代码块,使用乐观模式通常可以减少争用并提高吞吐量。
- 锁降
锁饥饿案例:假如当前有1000个线程,999个在进行读操作,1个在进行写操作。有可能999个读操作长期获取锁,导致1个写操作长时间获取不到锁。导致锁饥饿
ReentrantReadWriteLock 和 StampedLock对比:
ReentrantReadWVriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态。
读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock文持读并发,读读可以共享
StampedLock
- ReentrantReadWriteLock的读锁被占用的时候,其他线程尝武获取写锁的时候会被阻塞。
- 但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阳塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。
对于短的只读代码块,使用乐观模式通常可以减少争用并提高吞吐量。
(1)特点
-
所有获取锁的方法,都返回一个邮戳
Stamp
,Stamp
为零表示获取失败,其余都表示成功; -
所有释放锁的方法都需要一个邮戳
Stamp
,这个Stamp
必须是和成功获取锁时得到的Stamp
一致; -
StampedLock
是不可重入的,危险 !!!
(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁) -
StampedLock
有三种访问模式- Reading(悲观读模式):和
ReentrantReadWriteLock.readLock().lock()
一样 - Writing(悲观写模式):和
ReentrantReadWriteLock.writeLock().lock()
一样 - Optimistic Reading(乐观读模式): 无锁机制,类似于数据库中的乐观锁,支持读写并发,乐观认为读取时没有人修改,如果被修改再升级为悲观读模式
- Reading(悲观读模式):和
乐观读模式:仅当锁定当前未处于写入模式时,方法
tryOptimisticead()
才返回非零戳记。如果自获得给定标记以来未在写入模式下获取锁定,则方法validate(long)返回tue。这种模式可以被认为是读锁的极弱版本,可以随时被作者,坏。对短的只读代码段使用乐观模式通常可以减少争,用并提高吞吐量。但是,它的使用本质上是脆弱的。乐观读取部分应该只读取字段并将它们保存在局部变量中,以便以后在验证后使用。在乐观摸式下读取的字段可能非常不一致,因此仅在您熟采数据表示以拾查一致性和/或重复调用方法validate()。例如,在首次读取对象或数组引用,然后访问其中一个字段,元素或方法时,通常需要执行此类步。
(2)代码演示
// writelong writeStamp = stampedLock.writeLock();
//业务代码
stampedLock.unlock(writeStamp);// read
long readStamp = stampedLock.readLock();
stampedLock.unlock(writeStamp);
- 普通读写
public void write(){long stamp = stampedLock.writeLock();System.out.println("WW 写线程准备修改");try{num = num + 1;}finally {stampedLock.unlockWrite(stamp);}System.out.println("WW 写线程修改结束");
}/*** 悲观读*/
public void read(){long stamp = stampedLock.readLock();System.out.println("RR 读线程准备读取");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}try {System.out.println("读线程读取" + num);}finally {stampedLock.unlockRead(stamp);}System.out.println("RR 读线程完成读取");
}
- 乐观读
/*** 乐观读*/
public void optimisticRead(){long stamp = stampedLock.tryOptimisticRead();int result = num;//故意间隔4秒 乐观认为读取中没有其他线程修改过number值,具体靠判断System.out.println("4s前stampedLock.validate方法(true无修改,false有修改): " + stampedLock.validate(stamp));for (int i = 0; i < 4; i++) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("OR 正在读取 " + i + "::" + num);System.out.println("OR STAMP Validate标记:" + stampedLock.validate(stamp));}if (!stampedLock.validate(stamp)) { // 有写操作,可以对其进行手动升级System.out.println("有人修改");long readStamp = stampedLock.readLock();try{System.out.println("OR RR 升级悲观读");result = num;System.out.println("OR RR 悲观读后 result " + result);}finally {stampedLock.unlockRead(readStamp);}}System.out.println("OR 最终值 " + result);
}
(3)缺点
- StampedLock不支持重入,没有Reentrant开头。读写锁原理上都不是可重入锁 ,读锁是伪重入,需要多次释放
- StampedLock的悲观读锁和写锁都不支持条件变量(Condition),
- 使用StampedLock一定不要调用中断操作,即不要调用
interrupt()
方法。