一、读写锁的核心价值

在多线程编程中,同步机制是保证线程安全的关键。传统的互斥锁(如synchronized)在读多写少的场景下存在明显性能瓶颈:读操作被不必要的串行化,即使多个线程只读取数据也会相互阻塞。这正是ReentrantReadWriteLock的用武之地!

读写锁的优势

  1. 读读并发:多个线程可以同时获取读锁
  2. 读写互斥:写锁独占时阻塞所有读写操作
  3. 写写互斥:同一时刻只允许一个写操作
  4. 锁降级:写锁可安全降级为读锁(本文重点)

二、ReentrantReadWriteLock实现原理

2.1 状态分离设计

ReentrantReadWriteLock通过AQS(AbstractQueuedSynchronizer)实现,其核心在于将32位state分为两部分:

// 状态位拆分示意
static final int SHARED_SHIFT = 16; // 共享锁移位值
static final int EXCLUSIVE_MASK = (1 << 16) - 1; // 独占锁掩码 // 获取读锁数量(高16位)
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
} // 获取写锁重入次数(低16位)
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}

2.2 锁获取规则

锁类型 获取条件
读锁 无写锁持有,或持有写锁的是当前线程(锁降级情况)
写锁 无任何读锁且无其他线程持有写锁(可重入)

2.3 工作流程对比

读锁获取流程:

1. 检查是否有写锁持有
├─ 无:增加读锁计数,获取成功
└─ 有:检查是否当前线程持有
├─ 是:获取成功(锁降级情况)
└─ 否:进入等待队列

写锁获取流程:

1. 检查是否有任何锁
├─ 无:设置写锁状态,获取成功
└─ 有:检查是否当前线程重入
├─ 是:增加写锁计数
└─ 否:进入等待队列

三、锁降级:原理与必要性

3.1 什么是锁降级?

锁降级(Lock Downgrading) 是指线程在持有写锁的情况下:

  1. 获取读锁
  2. 释放写锁
  3. 在仅持有读锁的状态下继续操作
// 标准锁降级流程
writeLock.lock(); // 1.获取写锁
try {
// 修改数据...
readLock.lock(); // 2.获取读锁(关键步骤)
} finally {
writeLock.unlock(); // 3.释放写锁(完成降级)
} try {
// 读取数据(受读锁保护)
} finally {
readLock.unlock(); // 4.释放读锁
}

3.2 为什么需要锁降级?

考虑以下无锁降级的危险场景:

时间线:
1. 线程A获取写锁
2. 线程A修改数据
3. 线程A释放写锁
4. [危险间隙开始]
5. 线程B获取写锁
6. 线程B修改数据
7. 线程B释放写锁
8. [危险间隙结束]
9. 线程A获取读锁
10. 线程A读取到线程B修改的数据(非预期!)

锁降级通过在释放写锁前获取读锁,消除了这个危险间隙:

时间线:
1. 线程A获取写锁
2. 线程A修改数据
3. 线程A获取读锁
4. 线程A释放写锁
5. [读锁保护中]
6. 线程B尝试获取写锁(阻塞)
7. 线程A安全读取数据
8. 线程A释放读锁
9. 线程B获取写锁

3.3 锁降级的核心价值

  1. 数据一致性:确保线程看到自己修改的最新数据
  2. 写后读原子性:消除写锁释放到读锁获取之间的危险窗口
  3. 并发性优化:允许其他读线程并发访问最新数据

四、完整代码示例

4.1 基础读写锁使用

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private int sharedData = 0; // 写操作
public void writeData(int value) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始写入: " + value);
sharedData = value;
Thread.sleep(100); // 模拟写耗时
System.out.println(Thread.currentThread().getName() + " 写入完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
writeLock.unlock();
}
} // 读操作
public void readData() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始读取");
Thread.sleep(50); // 模拟读耗时
System.out.println(Thread.currentThread().getName() + " 读取到: " + sharedData);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
readLock.unlock();
}
} public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo(); // 创建读线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
while (true) {
demo.readData();
sleep(200);
}
}, "Reader-" + i).start();
} // 创建写线程
for (int i = 0; i < 2; i++) {
int id = i;
new Thread(() -> {
int value = 0;
while (true) {
demo.writeData(value++);
sleep(300);
}
}, "Writer-" + id).start();
}
} private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

执行效果说明:

Reader-0 开始读取
Reader-1 开始读取 // 多个读线程可以并发
Reader-0 读取到: 0
Reader-1 读取到: 0
Writer-0 开始写入: 0 // 写操作独占
Writer-0 写入完成
Reader-2 开始读取
Reader-3 开始读取 // 写完成后读操作恢复并发
Reader-2 读取到: 0
Reader-3 读取到: 0

4.2 锁降级实战

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDowngradeDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private volatile boolean dataValid = false;
private int criticalData = 0; public void processWithDowngrade() {
// 1. 获取写锁
writeLock.lock();
try {
// 2. 准备数据(写操作)
System.out.println("[" + Thread.currentThread().getName() + "] 获取写锁,准备数据...");
prepareData(); // 3. 获取读锁(开始降级)
readLock.lock();
System.out.println("[" + Thread.currentThread().getName() + "] 获取读锁(准备降级)");
} finally {
// 4. 释放写锁(保留读锁)
writeLock.unlock();
System.out.println("[" + Thread.currentThread().getName() + "] 释放写锁(完成降级)");
} try {
// 5. 使用数据(读操作)
System.out.println("[" + Thread.currentThread().getName() + "] 在降级保护下使用数据");
useData();
} finally {
// 6. 释放读锁
readLock.unlock();
System.out.println("[" + Thread.currentThread().getName() + "] 释放读锁");
}
} private void prepareData() {
// 模拟数据准备(写操作)
criticalData = (int) (Math.random() * 1000);
dataValid = true;
sleep(500); // 模拟耗时操作
} private void useData() {
if (!dataValid) {
System.err.println("数据无效!");
return;
} // 模拟数据使用(读操作)
System.out.println(">>> 使用关键数据: " + criticalData + " <<<");
sleep(300);
} // 干扰线程:尝试修改数据
public void disturb() {
writeLock.lock();
try {
System.out.println("\t[" + Thread.currentThread().getName() + "] 干扰线程获取写锁!");
criticalData = -1; // 破坏数据
dataValid = false;
} finally {
writeLock.unlock();
}
} public static void main(String[] args) {
LockDowngradeDemo demo = new LockDowngradeDemo(); // 主工作线程(执行锁降级)
new Thread(() -> demo.processWithDowngrade(), "MainWorker").start(); sleep(100); // 确保主线程先启动 // 干扰线程
new Thread(() -> {
System.out.println("\t[Disturber] 尝试干扰...");
demo.disturb();
System.out.println("\t[Disturber] 干扰完成");
}, "Disturber").start();
} private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

执行效果说明:

[MainWorker] 获取写锁,准备数据...
[Disturber] 尝试干扰... // 干扰线程启动
[MainWorker] 获取读锁(准备降级)
[MainWorker] 释放写锁(完成降级)
[MainWorker] 在降级保护下使用数据
>>> 使用关键数据: 742 <<< // 数据未被干扰
[MainWorker] 释放读锁
[Disturber] 干扰线程获取写锁! // 此时才获取写锁
[Disturber] 干扰完成

4.3 错误示例:忘记锁降级

public void flawedProcess() {
writeLock.lock();
try {
prepareData();
} finally {
writeLock.unlock(); // 危险:先释放写锁
} // 此时其他线程可能修改数据!
readLock.lock();
try {
useData(); // 可能使用过期数据
} finally {
readLock.unlock();
}
}

风险分析:

时间线:
1. 线程A获取写锁
2. 线程A修改数据
3. 线程A释放写锁
4. [危险间隙]
5. 线程B获取写锁
6. 线程B修改数据
7. 线程B释放写锁
8. 线程A获取读锁
9. 线程A读取到过期数据(线程B修改后的数据)

五、关键注意事项

  1. 严格顺序:写锁 → 读锁 → 释放写锁(不可颠倒)
  2. 不支持升级:读锁不能直接升级为写锁(会导致死锁)
  3. 锁范围:降级后的读锁保护范围应尽量小
  4. 异常处理:始终在finally块中释放锁
  5. 性能考量:读写锁适用于读多写少场景(写频繁时性能可能不如互斥锁)

六、总结

ReentrantReadWriteLock通过读写分离的设计显著提升读多写少场景的性能:

  • 高16位记录读锁数量,低16位记录写锁重入次数
  • 读读不互斥,读写/写写互斥
  • 锁降级确保写后读操作的数据一致性

锁降级是读写锁应用中的高级技巧,它通过:

  1. 写锁中获取读锁
  2. 先释放写锁保留读锁
  3. 在读锁保护下完成后续操作

这种机制消除了写后读操作之间的危险间隙,在金融交易、配置更新等需要强一致性的场景中尤为重要。正确使用锁降级,既能保证数据一致性,又能最大化并发性能,是高级Java开发者必备的并发技能。

【深入理解ReentrantReadWriteLock】读写分离与锁降级实践的更多相关文章

  1. java多线程:并发包中ReentrantReadWriteLock读写锁的锁降级模板

    写锁降级为读锁,但读锁不可升级或降级为写锁. 锁降级是为了让当前线程感知到数据的变化. //读写锁 private ReentrantReadWriteLock lock=new ReentrantR ...

  2. MySQL主从复制(Master-Slave)与读写分离(MySQL-Proxy)实践

    主服务器上(注:应该是允许从机访问)  GRANT REPLICATION SLAVE ON *.* to ‘rep1’@’192.168.10.131’ identified by ‘passwor ...

  3. Linux下Mysql主从复制(Master-Slave)与读写分离(Amoeba)实践

    一.为什么要做Mysql的主从复制(读写分离)?通俗来讲,如果对数据库的读和写都在同一个数据库服务器中操作,业务系统性能会降低.为了提升业务系统性能,优化用户体验,可以通过做主从复制(读写分离)来减轻 ...

  4. [转]MySQL主从复制(Master-Slave)与读写分离(MySQL-Proxy)实践

    转自:http://heylinux.com/archives/1004.html Mysql作为目前世界上使用最广泛的免费数据库,相信所有从事系统运维的工程师都一定接触过.但在实际的生产环境中,由单 ...

  5. MHA + Maxscale 数据库的高可用和读写分离

    MySQL 常见发行版本 MySQL 标准化.自动化部署 深入浅出MySQL备份与恢复 深入理解MySQL主从复制 MySQL构架设计与容量规划 MHA Maxscale MySQL 常见发行版本 M ...

  6. MySQL的读写分离的几种选择

    MySQL的读写分离的几种选择 MySQL主从复制(Master-Slave)与读写分离(MySQL-Proxy)实践 原址如下: http://heylinux.com/archives/1004. ...

  7. MySQL主从复制与读写分离实践

    MySQL主从复制(Master-Slave)与读写分离(MySQL-Proxy)实践  目录: 介绍 MySQL的安装与配置 MySQL主从复制 MySQL读写分离 编译安装lua 安装配置MySQ ...

  8. 使用Atlas实现MySQL读写分离+MySQL-(Master-Slave)配置

    参考博文: MySQL-(Master-Slave)配置  本人按照博友北在北方的配置已成功  我使用的是 mysql5.6.27版本. 使用Atlas实现MySQL读写分离 数据切分——Atlas读 ...

  9. 读写分离,读写分离死锁解决方案,事务发布死锁解决方案,发布订阅死锁解决方案|事务(进程 ID *)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务

    前言:         由于网站访问压力的问题,综合分析各种因素后结合实际情况,采用数据库读写分离模式来解决当前问题.实际方案中采用“事务发布”模式实现主数据库和只读数据库的同步,其中: 发布服务器1 ...

  10. ReentrantReadWriteLock可重入,锁升级,锁降级

    public class ReentrantReadWriteLockTest { public static void main(String[] args) throws InterruptedE ...

随机推荐

  1. 🎀Java-Exception与RuntimeException

    简介 Exception Exception 类是所有非致命性异常的基类.这些异常通常是由于编程逻辑问题或外部因素(如文件不存在.网络连接失败等)导致的,可以通过适当的编程手段来恢复或处理.Excep ...

  2. eolinker请求参数:提交参数JSON转换格式不正确的解决方法

    当某个接口的提交参数类型为"array"时,该接口被自动化测试调用会转换成text类型. 导致执行测试的时候,整个参数转化json格式不正确 解决方法是在  格式不正确的项后面 配 ...

  3. MCP数据脱敏应用开发

    一.概述 数据脱敏(Data Masking),又称数据漂白.数据去隐私化或数据变形. 定义 指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护.在涉及客户安全数据或者一些商业性敏 ...

  4. C#/.NET/.NET Core技术前沿周刊 | 第 35 期(2025年4.14-4.20)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  5. HTML5和CSS3基础

    HTML元素 空元素 不是所有元素都拥有开始标签.内容和结束标签.一些元素只有一个标签,通常用来在此元素所在位置插入/嵌入一些东西.这些元素被称为空元素例如:元素 `` 是用来在页面插入一张指定的图片 ...

  6. 『Plotly实战指南』--在金融数据可视化中的应用(下)

    在金融市场的复杂博弈中,可视化技术如同精密的导航仪. 传统静态图表正在被交互式可视化取代--据Gartner研究,采用动态可视化的投资机构决策效率提升达47%. 本文的目标是探讨如何利用 Plotly ...

  7. TensorFlow 基础 (04)

    最近都面临一个问题是, 要用纯 sql 来实现所有的逻辑, 其实 union 呀, 嵌套, 子查询呀, 这些都还好, 但那带有逻辑判断的, 这就整不好整了, 就多分支的, 再分支这种... 也不知为啥 ...

  8. B1037 在霍格沃茨找零钱

    如果你是哈利·波特迷,你会知道魔法世界有它自己的货币系统 -- 就如海格告诉哈利的:"十七个银西可(Sickle)兑一个加隆(Galleon),二十九个纳特(Knut)兑一个西可,很容易.& ...

  9. Hitachi Vantara Programming Contest 2024(AtCoder Beginner Contest 368)题解A~D

    A - Cut 题意: 将数组的后k个字符移到前面 思路: 可以用rotate()函数让数组中的元素滚动旋转 rotate(v.begin(), v.begin() + n - k, v.end()) ...

  10. Windows平台调试器原理与编写05.内存断点

    https://www.bpsend.net/thread-274-1-3.html 内存断点 访问断点 写入断点 内存写入断点 简介:当被调试进程访问,读或写指定内存的时候,程序能够断下来. 思考1 ...