当前位置: 首页 > news >正文

第十三章 ReentrantLock、ReentrantReadWriteLock、StampedLock 讲解

13.1 关于锁的面试题

  • 你知道 Java 里面有哪些锁?

  • 你说你用过读写锁,锁饥饿问题是什么?

  • 有没有比读写锁更快的锁?

  • StampedLock 知道吗?(邮戳锁/票据锁)

  • ReentrantReadWriteLock 有锁降级机制,你知道吗?

13.2 本章路线总纲

无锁 -> 独占锁 -> 读写锁 -> 邮戳锁

13.3 简单聊聊 ReentrantReadWriteLock

13.3. 1 是什么?

读写锁说明

一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

一体两面,读写互斥,读读共享

再说说演变

无锁无序 -> 加锁 -> 读写锁演变复习

读写锁意义和特点

它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是“读/读”线程间并不存在互斥关系

只有“读/写”线程或“写/写”线程间的操作需要互斥的。因此引入 ReentrantReadWriteLock

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

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

13.3.2 特点

  • 可重入
  • 读写兼顾

代码演示

ReentrantLock 存在的问题,即读读不共享,影响效率

public class ReentrantReadWriteLockDemo {/*** 运行结果:* 0	 正在写入* 0	 写入完成* 1	 正在写入* 1	 写入完成* ...* 0	 正在读取* 0	 读取完成* 1	 正在读取* 1	 读取完成* ...*/public static void main(String[] args) {MyResource myResource = new MyResource();for (int i = 0; i < 10; i++) {int finalI = i;new Thread(() -> {myResource.write(finalI + "", finalI + "");}, String.valueOf(i)).start();}for (int i = 0; i < 10; i++) {int finalI = i;new Thread(() -> {myResource.read(finalI + "");}, String.valueOf(i)).start();}}}//资源类,模拟一个简单的缓存
class MyResource {Map<String, String> map = new HashMap<>();//====ReentrantLock 等价于 ==== synchronizedLock lock = new ReentrantLock();public void write(String key, String value) {lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t 正在写入");map.put(key, value);try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t 写入完成");} finally {lock.unlock();}}public void read(String key) {lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t 正在读取");String result = map.get(key);try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t 读取完成");} finally {lock.unlock();}}}

使用 ReentrantReadWriteLock,解决读读共享问题

public class ReentrantReadWriteLockDemo {/*** 运行结果:* 0	 正在写入* 0	 写入完成* 1	 正在写入* 1	 写入完成* ...* 3	 正在读取* 4	 正在读取* 5	 正在读取* 9	 正在读取* 5	 读取完成* 3	 读取完成* 4	 读取完成* 9	 读取完成* ...*/public static void main(String[] args) {MyResource myResource = new MyResource();for (int i = 0; i < 10; i++) {int finalI = i;new Thread(() -> {myResource.write(finalI + "", finalI + "");}, String.valueOf(i)).start();}for (int i = 0; i < 10; i++) {int finalI = i;new Thread(() -> {myResource.read(finalI + "");}, String.valueOf(i)).start();}}}//资源类,模拟一个简单的缓存
class MyResource {Map<String, String> map = new HashMap<>();//====ReentrantReadWriteLock 一体两面,读写互斥,读读共享ReadWriteLock rwLock = new ReentrantReadWriteLock();public void write(String key, String value) {rwLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "\t 正在写入");map.put(key, value);try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t 写入完成");} finally {rwLock.writeLock().unlock();}}public void read(String key) {rwLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + "\t 正在读取");String result = map.get(key);try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t 读取完成");} finally {rwLock.readLock().unlock();}}}

读没有完成时,其他线程写锁无法获得

public class ReentrantReadWriteLockDemo {/*** 运行结果:* 0	 正在写入* 0	 写入完成* 2	 正在写入* 2	 写入完成* ...* 0	 正在读取* 3	 正在读取* ...* 3	 读取完成* 0	 读取完成* ...* 新写锁线程:1	 正在写入* 新写锁线程:1	 写入完成* 新写锁线程:0	 正在写入* 新写锁线程:0	 写入完成* 新写锁线程:2	 正在写入* 新写锁线程:2	 写入完成*/public static void main(String[] args) {MyResource myResource = new MyResource();for (int i = 0; i < 10; i++) {int finalI = i;new Thread(() -> {myResource.write(finalI + "", finalI + "");}, String.valueOf(i)).start();}for (int i = 0; i < 10; i++) {int finalI = i;new Thread(() -> {myResource.read(finalI + "");}, String.valueOf(i)).start();}//暂停 1 秒try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}for (int i = 0; i < 3; i++) {int finalI = i;new Thread(() -> {myResource.write(finalI + "", finalI + "");}, "新写锁线程:" + String.valueOf(i)).start();}}}//资源类,模拟一个简单的缓存
class MyResource {Map<String, String> map = new HashMap<>();//====ReentrantReadWriteLock 一体两面,读写互斥,读读共享ReadWriteLock rwLock = new ReentrantReadWriteLock();public void write(String key, String value) {rwLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "\t 正在写入");map.put(key, value);try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t 写入完成");} finally {rwLock.writeLock().unlock();}}public void read(String key) {rwLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + "\t 正在读取");String result = map.get(key);//演示读锁没有完成之前,写锁无法获得try {TimeUnit.MILLISECONDS.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t 读取完成");} finally {rwLock.readLock().unlock();}}}

结论:一体两面,读写互斥,读读共享,读没有完成的时候其他线程写锁无法获得

出现写锁饥饿的问题

锁降级

从写锁 -> 读锁,ReentrantReadWriteLock 可以降级

《Java 并发编程的艺术》中关于锁降级的说明

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

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

写锁的降级,降级成为了读锁

  1. 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁
  2. 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序
  3. 如果释放了写锁,那么就完全转换为读锁

锁降级:遵循获取写锁 -> 再获取读锁 -> 再释放写锁的次序,写锁能够降级成为读锁

如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁

image

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

锁降级是为了让当前线程感知到数据的变化,目的是保证数据的可见性

读写锁降级演示

正常情况

public class LockDownGradingDemo {/*** 运行结果:* ----读取* ----写入*/public static void main(String[] args) {ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//正常 A B 两个线程//AreadLock.lock();System.out.println("----读取");readLock.unlock();//BwriteLock.lock();System.out.println("----写入");writeLock.unlock();}}

锁降级,写锁降级为读锁

/*** 锁降级:遵循获取写锁->再获取读锁->再释放写锁的次序,写锁能够降级成为读锁* 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁*/
public class LockDownGradingDemo {/*** 运行结果:* ----写入* ----读取*/public static void main(String[] args) {ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//本例,only one 同一个线程writeLock.lock();System.out.println("----写入");readLock.lock();System.out.println("----读取");writeLock.unlock();readLock.unlock();}}

读没有完成时候写锁无法获得锁,必须要等着读锁读完后才有机会写

/*** 锁降级:遵循获取写锁->再获取读锁->再释放写锁的次序,写锁能够降级成为读锁* 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁* 读没有完成时候写锁无法获得锁,必须要等着读锁读完后才有机会写*/
public class LockDownGradingDemo {/*** 运行结果:* ----读取* 程序阻塞!*/public static void main(String[] args) {ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();/*//正常 A B 两个线程//AreadLock.lock();System.out.println("----读取");readLock.unlock();//BwriteLock.lock();System.out.println("----写入");writeLock.unlock();*///本例,only one 同一个线程readLock.lock();System.out.println("----读取");writeLock.lock();System.out.println("----写入");writeLock.unlock();readLock.unlock();}}

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

线程获取读锁是不能直接升级为写入锁的

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

写锁和读锁是互斥的

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作

因此,分析读写锁 ReentrantReadWriteLock,会发现它有个潜在的问题:

读锁结束,写锁有望;写锁独占,读写全堵

如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁

即 ReentrantReadWriteLock 读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁,人家还在读着呢,你先别去写,省的数据乱

13.3.3 读写锁之后读写规矩,解释为什么要锁降级

ReentrantWriteReadLock 源码总结

class CacheData {Object data;volatile boolean cacheValid;final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock;void processCachedData() {// 第1步: 获取读锁,检查缓存是否有效rwl.readLock().lock();if (!cacheValid) {//  关键: 必须先释放读锁,才能获取写锁rwl.readLock().unlock();// 第2步: 获取写锁,更新缓存rwl.writeLock().lock();   //try {// 双重检查: 其他线程可能已经更新了缓存if (!cacheValid) {data = ...;              // 从数据库加载数据cacheValid = true;       // 标记缓存有效}// 第3步: 锁降级 - 在释放写锁前获取读锁rwl.readLock().lock();} finally {// 第4步: 释放写锁,但仍持有读锁rwl.writeLock().unlock();}}// 第5步: 使用数据(此时持有读锁)try {use(data);} finally {// 第6步: 最终释放读锁rwl.readLock().unlock();}}}
  1. 代码中声明了一个 volatile 类型的 cacheValid 变量,保证其可见性

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

总结:一句话,同一个线程自己持有写锁时再去拿读锁,其本质相当于重入

如果违背锁降级的步骤

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

如果遵循锁降级的步骤

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

有没有比读写锁更快的锁?

13.4 邮戳锁 StampedLock

无锁 -> 独占锁 -> 读写锁 -> 邮戳锁

13.4.1 是什么?

StampedLock 是 JDK1.8 中新增的一个读写锁,也是对 JDK1.5 中的读写锁 ReentrantReadWriteLock 的优化

邮戳锁,也叫票据锁

stamp(戳记,long 类型)

代表了锁的状态。当 stamp 返回零的时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的 stamp 值

13.4.2 它是由锁饥饿问题引出

锁饥饿问题

ReentrantReadWriteLock 实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了

假如当前 1000 个线程,999 个读,1 个写,有可能 999 个读线程长时间抢到了锁,那 1 个写线程就悲剧了

因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写

如何缓解锁饥饿问题?

使用“公平”策略可以一定程度上解决这个问题,如:new ReentrantReadWriteLock(true)

但是“公平”策略是以牺牲系统吞吐量为代价的

StampedLock 类的乐观读锁闪亮登场

  • ReentrantReadWriteLock:

    允许多个线程同时读,但是只允许一个线程写,再线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的 synchronized 速度要快很多,原因就是在于 ReentrantReadWriteLock 支持读并发,读读可以共享

  • StampedLock 横空出世:

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

http://www.jsqmd.com/news/707963/

相关文章:

  • 终极指南:DevDocs如何突破性能瓶颈应对海量用户访问挑战
  • GLM-4-9B-Chat-1M效果展示:1M上下文下多角色对话状态持久化演示
  • 用Python的Turtle库画樱花树:从零到一的图形化编程实战(附完整源码)
  • 基于模板驱动的PPT自动化生成:解放重复劳动,实现高效办公
  • 2026空气炸锅哪个品牌质量比较好?真实使用体验测评 - 品牌排行榜
  • 基于Java的MBTI性格测试系统的设计与实现
  • Rodio错误处理:如何优雅处理音频播放中的各种异常
  • 终极Material Design Lite CI/CD指南:使用GitHub Actions实现自动化构建与测试
  • Django REST Framework反向解析:动态生成API链接的终极指南
  • AIFS-model - little
  • 解锁XYFlow界面自由:6大方位自定义面板的实战指南
  • Livegrep企业级应用:如何集成到CI/CD流程和开发者工作流中
  • VASP计算半导体带隙不准?试试HSE06杂化泛函的保姆级四步法(附完整INCAR)
  • 盒马鲜生购物卡别浪费,教你正确回收方式! - 团团收购物卡回收
  • KiCad 3D视图太“秃然”?用立创EDA的现成模型让你的PCB“丰满”起来(附.3dshapes文件夹避坑指南)
  • 2026公积金咨询公司推荐,公积金咨询注意事项!公积金咨询公司优选指南! - 速递信息
  • 别再纠结选哪个了!Asterisk、FreeSWITCH、Kamailio、OpenSIPS四大开源SIP服务器保姆级对比(附选型指南)
  • Blueprint:为AI编码代理设计的冷启动规划系统,解决跨会话失忆难题
  • Pixel Dream Workshop 不同开源模型的横向对比:SDXL、SD 1.5与自定义模型
  • 告别手动维护!SAP ME_INFORECORD_MAINTAIN BAPI批导采购信息记录保姆级教程
  • 保姆级教程:在RuoYi-Vue-Pro项目中,从零搭建一个请假审批工作流(Flowable实战)
  • 回收华润万家购物卡避坑指南:小白必看实用干货 - 团团收购物卡回收
  • org-roam-ui API 详解:构建自定义集成与扩展
  • 天津猎头公司前十名推荐!哪家猎头公司做得最好? - 榜单推荐
  • jq数据聚合终极指南:多源JSON数据的合并与汇总技巧
  • 在Ubuntu上5分钟搞定OpenHarmony 4.0轻量系统到QEMU RISC-V的编译(附Python 3.10报错修复)
  • 终极A/B测试指南:揭秘Netflix与Amazon如何设计大规模实验
  • EzySlice 与 Unity3D 2018+ 的完美集成:完整部署与配置教程
  • 超分模型训练数据怎么选?深度对比BSRGAN、Real-ESRGAN和SwinIR的数据配方
  • 2026年抗菌板公司推荐及选购参考/医疗抗菌板,医院抗菌板,木纹抗菌板索洁板,冰火板 - 品牌策略师