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

JUC多线程:读写锁

文章提示:本文包含大量代码案例及生活化比喻,适合刚接触锁机制的新手。建议边阅读边动手实践,效果更佳!


 目录

文章前言

一、锁的分类与核心概念

1. 悲观锁 vs 乐观锁

2. 表锁 vs 行锁(数据库层面)

3. 读锁 vs 写锁(JUC)

防止死锁的策略

 二、读写锁(ReadWriteLock)

1. 核心规则

 2. 代码案例:缓存系统

代码分析 :

执行流程:

三、锁降级(Lock Downgrading)

1. 什么是锁降级?

2. 代码示例

四、总结与进阶

1. 如何选择锁?

2. 避坑指南

3. 实战技巧

文章总结


文章前言

在多线程编程中,锁机制是保证数据安全的核心工具。但面对“读写锁”“乐观锁”“表锁行锁”这些概念时,很多初学者会感到困惑:它们有什么区别?什么时候该用哪种锁?本文将通过通俗易懂的语言、生活化案例和代码演示,带你彻底搞懂Java中的各类锁机制,让你写出更高效、更安全的并发程序!


一、锁的分类与核心概念

1. 悲观锁 vs 乐观锁
  • 悲观锁:像“老管家”,总担心有人搞破坏。

    • 行为:每次操作数据前先加锁(比如synchronized)。

    • 适用场景:写操作频繁,冲突概率高。

    // 示例:synchronized实现悲观锁
    public synchronized void updateData() {// 写操作
    }
  • 乐观锁:像“乐观青年”,相信冲突很少发生。

    • 行为:先操作数据,提交时检查是否冲突(如CAS算法)。

    • 适用场景:读多写少,冲突概率低。

    // 示例:AtomicInteger使用CAS(乐观锁思想)
    AtomicInteger count = new AtomicInteger(0);
    count.incrementAndGet(); // 内部通过CAS实现

2. 表锁 vs 行锁(数据库层面)
  • 表锁:锁住整张表(像“封楼”)。

    • 特点:简单粗暴,但并发性能差。

  • 行锁:只锁住某一行数据(像“封教室”)。

    • 特点:粒度细,并发性能好。


3. 读锁 vs 写锁(JUC)

读锁:适合于只读操作,并允许同一时间多个线程同时持有。

共享锁,发生死锁   

虽然读锁是共享的,并且允许多个线程同时持有,但在特定情况下,依然可能形成死锁。一个常见的场景涉及多个资源以及嵌套的获取锁的操作。

示例:

假设有两个共享资源 ResourceAResourceB,每个资源都有自己的读写锁。考虑以下情况:

  1. 线程T1首先获取了ResourceA的读锁,然后尝试获取ResourceB的读锁。
  2. 同时,线程T2首先获取了ResourceB的读锁,然后尝试获取ResourceA的读锁。

如果这两个操作交错执行,即T1持有了ResourceA的读锁但等待ResourceB的读锁,而T2持有了ResourceB的读锁但等待ResourceA的读锁,这将不会直接导致死锁,因为读锁是可以被多个线程持有的。然而,如果有其他条件或锁加入到这个过程中,比如某个线程需要升级读锁为写锁,那么就可能引发死锁。

写锁:用于修改操作,要求独占访问,确保数据一致性。

写锁:独占锁,发生死锁

写锁由于其独占性质,在多线程环境中更容易造成死锁问题,特别是当涉及到多个锁并且这些锁以不同的顺序请求时。

示例:

假设我们有两个共享资源 ResourceXResourceY,每个资源都有自己的读写锁。现在考虑以下情况:

  1. 线程T1首先获取了ResourceX的写锁,然后尝试获取ResourceY的写锁。
  2. 同时,线程T2首先获取了ResourceY的写锁,然后尝试获取ResourceX的写锁。

如果这两个操作交错执行,即T1持有了ResourceX的写锁但等待ResourceY的写锁,而T2持有了ResourceY的写锁但等待ResourceX的写锁,这样就会形成典型的循环等待条件,导致死锁。

防止死锁的策略

为了避免上述死锁的情况,可以采取一些预防措施:

  • 固定顺序加锁:确保所有线程总是按照相同的顺序来获取锁。
  • 尝试锁定:使用非阻塞的方式尝试获取锁,如Java中的tryLock()方法,允许你在无法立即获得锁时回退并重试。
  • 锁超时:设置锁的获取超时时间,超过一定时间未能获得锁则放弃尝试,避免无限期等待。

通过合理设计程序逻辑和谨慎管理锁的获取与释放过程,可以在很大程度上减少甚至避免死锁的发生。


 二、读写锁(ReadWriteLock)

1. 核心规则

ReadWriteLock 是 Java 并发工具包(java.util.concurrent.locks)中的一种接口,提供了读锁(ReadLock)和写锁(WriteLock)两种类型的锁。读锁是共享锁,允许多个线程同时读取数据;写锁是独占锁,一次只能被一个线程持有。ReadWriteLock 通过这种方式提高了并发性能,特别是在读多写少的场景中。

定义:ReadWriteLock 接口提供了读锁和写锁,分别通过 readLock() 和 writeLock() 方法获取。
工作原理

  • 读锁(ReadLock):允许多个线程同时持有,适用于读操作。
  • 写锁(WriteLock):一次只能被一个线程持有,适用于写操作。

互斥规则:

  • 读-读:可以共存,多个线程可以同时读取数据。
  • 读-写:不能共存,写操作会阻塞所有读操作。
  • 写-写:不能共存,写操作会阻塞其他写操作。
操作组合是否兼容生活案例
读 + 读多人同时阅读同一本书
读 + 写有人写书时禁止阅读
写 + 写只能有一个人修改书内容

 2. 代码案例:缓存系统

package JUC.rw;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*** 独占锁(写锁) 一次只能被一个线程占有* 共享锁(读锁) 多个线程可以同时占有* ReadWriteLock* 读-读 可以共存!* 读-写 不能共存!* 写-写 不能共存!*/
public class ReadWriteLockDemo {public static void main(String[] args) {MyCacheLock myCache = new MyCacheLock();// 写入for (int i = 1; i <= 5 ; i++) {final int temp = i;new Thread(()->{myCache.put(temp+"",temp+"");},String.valueOf(i)).start();}// 读取for (int i = 1; i <= 5 ; i++) {final int temp = i;new Thread(()->{myCache.get(temp+"");},String.valueOf(i)).start();}}
}
// 加锁的
class MyCacheLock{private volatile Map<String,Object> map = new HashMap<>();// 读写锁: 更加细粒度的控制private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 存,写入的时候,只希望同时只有一个线程写public void put(String key,Object value){readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName()+"写入"+key);map.put(key,value);System.out.println(Thread.currentThread().getName()+"写入OK");} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.writeLock().unlock();}}// 取,读,所有人都可以读!public void get(String key){readWriteLock.readLock().lock();try {System.out.println(Thread.currentThread().getName()+"读取"+key);Object o = map.get(key);System.out.println(Thread.currentThread().getName()+"读取OK");} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.readLock().unlock();}}
}
/*** 自定义缓存实现读写锁*/
class MyCache{private volatile Map<String,Object> map = new HashMap<>();// 存,写public void put(String key,Object value){System.out.println(Thread.currentThread().getName()+"写入"+key);map.put(key,value);System.out.println(Thread.currentThread().getName()+"写入OK");}// 取,读public void get(String key){System.out.println(Thread.currentThread().getName()+"读取"+key);Object o = map.get(key);System.out.println(Thread.currentThread().getName()+"读取OK");}
}
代码分析 :

ReadWriteLockDemo 类展示了如何使用 ReadWriteLock 来实现一个自定义缓存 MyCacheLock。以下是关键点分析:
初始化 ReadWriteLock:

  private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

 创建一个 ReentrantReadWriteLock 实例,该实例实现了 ReadWriteLock 接口。
        ReentrantReadWriteLock 提供了可重入的读锁和写锁。
写操作 (put 方法)

  public void put(String key, Object value) {readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "写入" + key);map.put(key, value);System.out.println(Thread.currentThread().getName() + "写入OK");} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.writeLock().unlock();}}

 线程调用 readWriteLock.writeLock().lock() 方法获取写锁。
        获取写锁后,线程可以安全地写入数据。
        写操作完成后,调用 readWriteLock.writeLock().unlock() 方法释放写锁。
读操作 (get 方法)

  public void get(String key) {readWriteLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + "读取" + key);Object o = map.get(key);System.out.println(Thread.currentThread().getName() + "读取OK");} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.readLock().unlock();}}

 线程调用 readWriteLock.readLock().lock() 方法获取读锁。
        获取读锁后,线程可以安全地读取数据。
        读操作完成后,调用 readWriteLock.readLock().unlock() 方法释放读锁。
启动线程

  public static void main(String[] args) {MyCacheLock myCache = new MyCacheLock();// 写入for (int i = 1; i <= 5; i++) {final int temp = i;new Thread(() -> {myCache.put(temp + "", temp + "");}, String.valueOf(i)).start();}// 读取for (int i = 1; i <= 5; i++) {final int temp = i;new Thread(() -> {myCache.get(temp + "");}, String.valueOf(i)).start();}}

 启动 5 个线程进行写操作。
        启动 5 个线程进行读操作。

执行流程:

1.初始化 ReadWriteLock

  • 创建 ReentrantReadWriteLock 实例,初始状态下没有线程持有读锁或写锁。

2.启动写线程

  • 启动 5 个写线程,每个线程尝试获取写锁。
  • 假设线程 1 成功获取写锁,其他写线程会被阻塞。
  • 线程 1 打印 "写入1",写入数据,打印 "写入OK"。
  • 线程 1 释放写锁。

3.启动读线程

  • 启动 5 个读线程,每个线程尝试获取读锁。
  • 假设线程 2 成功获取读锁,其他读线程也可以获取读锁。
  • 线程 2 打印 "读取1",读取数据,打印 "读取OK"。
  • 线程 2 释放读锁。

4.继续执行

  • 假设线程 3 成功获取写锁,其他写线程和读线程会被阻塞。
  • 线程 3 打印 "写入2",写入数据,打印 "写入OK"。
  • 线程 3 释放写锁。

5.继续执行

  • 假设线程 4 和线程 5 成功获取读锁,其他读线程也可以获取读锁。
  • 线程 4 和线程 5 打印 "读取2",读取数据,打印 "读取OK"。
  • 线程 4 和线程 5 释放读锁。

6.所有线程完成

  • 所有写线程和读线程依次获取锁,执行写入和读取操作,最终完成所有任务。

输出结果:


三、锁降级(Lock Downgrading)

1. 什么是锁降级?

将写锁降为读锁

  • 场景:先获取写锁 → 再获取读锁 → 释放写锁→ 释放读锁

  • 目的:保证数据修改后,其他线程能立即看到最新结果

2. 代码示例
public class LockDowngradeDemo {private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private int data = 0;public void processData() {rwLock.writeLock().lock(); // 1. 获取写锁try {data = 42; // 修改数据rwLock.readLock().lock(); // 2. 获取读锁(锁降级开始)} finally {rwLock.writeLock().unlock(); // 3. 释放写锁}try {System.out.println("当前数据: " + data);} finally {rwLock.readLock().unlock(); // 4. 释放读锁}}
}

关键点:在释放写锁前获取读锁,确保后续操作能安全读取数据。


四、总结与进阶

1. 如何选择锁?
场景推荐锁类型原因
读多写少读写锁提高并发读性能
写冲突频繁悲观锁避免反复重试
简单操作且线程安全要求高synchronized编码简单,JVM自动优化
2. 避坑指南
  • 死锁预防:按固定顺序获取锁,设置超时时间。

  • 性能优化:尽量减少锁的持有时间。

  • 锁粒度:优先选择细粒度锁(如行锁 > 表锁)。

3. 实战技巧
// 使用tryLock避免死锁
if (lock.tryLock(1, TimeUnit.SECONDS)) {try {// 操作代码} finally {lock.unlock();}
} else {System.out.println("获取锁失败,执行其他逻辑");
}

文章总结

锁机制就像交通信号灯:读写锁是“潮汐车道”(读时放宽,写时严控),悲观锁是“全程红灯等待”,乐观锁是“绿灯时小心通过”。理解它们的原理后,再结合业务场景选择:

  1. 高并发读取(如商品详情页)→ 读写锁

  2. 秒杀库存扣减 → 乐观锁(CAS或版本号)

  3. 数据库转账操作 → 悲观锁(行级锁)

记住:没有“万能锁”,只有“最适合的锁”。动手运行文中的代码案例,调整线程数量观察输出结果,你会对锁机制有更深刻的理解!

http://www.xdnf.cn/news/132445.html

相关文章:

  • 【高频考点精讲】前端构建工具对比:Webpack、Vite、Rollup和Parcel
  • 淘宝 /天猫/1688|京东API 常用接口列表与申请方式解析
  • P12167 [蓝桥杯 2025 省 C/Python A] 倒水
  • 对接金蝶获取接口授权代码
  • 第3讲、大模型如何理解和表示单词:词嵌入向量原理详解
  • Blender好用的插件推荐汇总
  • 电脑温度怎么看 查看CPU温度的方法
  • Golang | 位运算
  • DELPHI实现dbTreeView的节点拖动并更新
  • 为什么说美颜SDK动态贴纸才是直播、短视频平台的下一个爆点?看完你就懂了!
  • 连续帧点云目标检测结果展示,python实现
  • 这个免费的AI插件,居然让我5分钟看完2小时的YouTube视频!
  • 大麦项目pro版本来袭!扫平面试中的一切疑难杂症!
  • 视频丨Google 最新 AI 眼镜原型曝光:轻量 XR+情境感知 AI 打造下一代计算平台
  • 【C语言练习】002. 理解C语言的基本语法结构
  • 存储新势力:助力DeepSeek一体机
  • GIT下载步骤
  • Base64编码原理:二进制数据与文本的转换技术
  • 因泰立H13激光雷达赋能垃圾发电厂,炉渣体积测量与装车智能化
  • 跨Linux发行版CPU指令集兼容性深度解析与实践指南
  • 一文读懂Nginx应用之 CentOS安装部署Nginx服务
  • 当智驾成标配,车企暗战升级|2025上海车展
  • 告别 “幻觉” 回答:RAG 中知识库与生成模型的 7 种对齐策略
  • CUDA编程之Grid、Block、Thread线程模型
  • 用 ESP32 模拟 Wiegand 刷卡器:开发门禁系统必备的小工具
  • 【CODEMATE】进制转换(transform) 粤港澳青少年信息学创新大赛 C/C++/Python 解题思路
  • WebUI可视化:第2章:技术基础准备
  • 日语学习-日语知识点小记-构建基础-JLPT-N4阶段(11): てあります。
  • 《拆掉思维里的墙》 古典-摘抄
  • k8s(9) — zookeeper集群部署(亲和性、污点与容忍测试)