当前位置: 首页 > 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. 数据库转账操作 → 悲观锁(行级锁)

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

相关文章:

  • Pycharm(十六)面向对象进阶
  • python21-循环小作业
  • YOLOv8环境安装(超细全过程)
  • LeetCode -- Flora -- edit 2025-04-25
  • C++入侵检测与网络攻防之暴力破解
  • 项目笔记1:通用 Service的常见方法
  • 通讯录完善版本(详细讲解+源码)
  • 什么是财务管理系统?一文看清其功能及作用!
  • 【AI落地应用实战】借助 Amazon Q 实现内容分发网络(CDN)CDK 构建的全流程实践
  • 腾讯一面面经:总结一下
  • 玉米产量遥感估产系统的开发实践(持续迭代与更新)
  • 《人月神话》50周年遇到AI-那些乐趣和苦恼(01-03)
  • CF-Hero:自动绕过CDN找真实ip地址
  • 计算机组成原理第二章 数据的表示和运算——2.1数制与编码
  • 当智驾成标配,车企暗战升级|2025上海车展
  • 软件技术专业
  • 云服务器和独立服务器的区别在哪
  • 问答页面支持拖拽和复制粘贴文件,MaxKB企业级AI助手v1.10.6 LTS版本发布
  • 算能BM1684升级为BM1688: tpu_mlir转换模型_SDK更新_代码修改_问题排查_代码调试
  • 【MySQL】3分钟解决MySQL深度分页问题
  • 预热苏杯,“谁羽争锋”全国新闻界羽毛球团体邀请赛厦门开赛
  • 胃病、闭经、湿疹、失明:藏在疾病后的情绪问题
  • 大卫·第艾维瑞谈历史学与社会理论③丨尼古拉斯·卢曼与历史研究
  • 美国那点事|特朗普的“刀”砍向国务院,美国霸权迎来历史拐点?
  • 上海一小学百名学生齐聚图书馆:纸质书的浪漫AI无法取代
  • 新任遂宁市委副书记王忠诚已任市政府党组书记