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

JUC读写锁

文章目录

  • 一、读写锁概述
    • 1.1 核心目标
    • 1.2 核心思想
    • 1.3 关键规则与保证
    • 1.4 核心组件
  • 二、使用示例
    • 2.1 采用独占锁的姿势读、写数据
    • 2.2 使用读写锁读、写数据
    • 2.3 锁降级 **(Lock Downgrading)**
  • 三、应用场景
    • 3.1 缓存系统【高频读、低频更新】
    • 3.2 配置中心【配置读取多、更新少】
    • 3.3 金融交易系统【账户查询多、转账少】
    • 3.4 实时数据看板【数据读取多、更新少】
  • 四、场景分析图表
    • 4.1 读写锁在缓存系统的工作流程
    • 4.2 锁降级过程图解
    • 4.3 不同场景下锁的选择
  • 五、最佳实践与注意事项
    • 5.1 锁选择原则
    • 5.2 避免常见陷阱
    • 5.3 性能调优技巧
    • 5.4 替代解决方案
  • 六、典型应用场景总结
  • 七、没有了

一、读写锁概述

在这里插入图片描述

Java 中读写锁(ReadWriteLock)的核心概念。读写锁是 Java 并发包(java.util.concurrent.locks)中解决特定并发场景下性能问题的重要工具。

1.1 核心目标

  • 提高【读多写少】场景下的并发性能

想象一个共享资源(比如一个配置字典、一个缓存或一个大型数据集):

  • 读操作read):通常不会修改数据,只是获取数据。多个线程同时读取同一个数据通常是安全的
  • 写操作write):会修改数据。为了保证数据一致性,写操作通常需要独占访问资源。即写操作进行时,不允许其他任何线程(读或写)访问资源。

传统的互斥锁(如 synchronizedReentrantLock)在访问共享资源时,无论读写,都只允许一个线程访问。这在“读多写少”的场景下会造成巨大的性能瓶颈大量只读线程被强制串行执行,即使它们之间本可以安全地并发读取

1.2 核心思想

读写锁的核心思想:分离读锁和写锁

读写锁巧妙地解决了这个问题,它维护了一对锁

  • 读锁(共享锁,read lock

    • 共享性:多个线程可以同时持有读锁。
    • 目的:允许多个线程并发地读取共享资源,极大地提升读取的吞吐量。
    • 约束:当一个(或多个)线程持有读锁时,任何线程都无法获取写锁。这是为了保证读取数据的一致性——防止在读取过程中数据被修改。
  • 写锁(独占锁,write lock

    • 排他性:同一时刻只能有一个线程持有写锁。

    • 目的:保证写操作的原子性和数据一致性。写操作需要独占访问资源。

    • 约束

      • 当一个线程持有写锁时,其他任何线程(无论是想读还是想写)都无法获取读锁或写锁
      • 在获取写锁之前,必须等待所有已持有的读锁释放。同样,在获取读锁之前,必须等待已持有的写锁释放

1.3 关键规则与保证

读写锁的行为严格遵循以下规则,这些规则是理解其并发语义的基础:

  1. 读读共享(Read-Read Sharing):多个线程可以同时获取并持有读锁,进行并发读取操作。这是性能提升的关键。

  2. 读写互斥(Read-Write Mutual Exclusion)

  • 如果一个线程持有读锁,另一个线程尝试获取写锁会被阻塞,直到所有读锁释放。
  • 如果一个线程持有写锁,另一个线程尝试获取读锁会被阻塞,直到写锁释放。
  1. 写写互斥(Write-Write Mutual Exclusion):同一时刻只能有一个线程持有写锁。尝试获取写锁的线程会被阻塞,直到当前写锁释放。

  2. 可重入性(Reentrancy)

    • Java 的标准实现 ReentrantReadWriteLock 支持锁的可重入
    • 一个线程可以重复获取它已经持有的读锁或写锁(需要相应次数的释放)。
    • 持有写锁的线程可以再次获取读锁(锁降级的关键)。
    • 持有读锁的线程不能直接获取写锁(尝试获取写锁会阻塞,可能导致死锁)。如果需要升级,必须先释放所有读锁,再尝试获取写锁,但这个操作不是原子的,中间状态可能被其他写线程抢占。因此,锁升级通常不被推荐,且标准实现不支持。
  3. 公平性(Fairness)

  • 类似于 ReentrantLockReentrantReadWriteLock 可以构造为公平锁非公平锁(默认)。
  • 公平锁:线程按照请求锁的顺序(近似 FIFO)获取锁。这有助于避免线程饥饿(如写线程一直被读线程抢占),但可能降低整体吞吐量。
  • 非公平锁:允许“插队”。当锁可用时,一个等待线程(无论等待了多久)可能比更早等待的线程优先获得锁。这能提高吞吐量,但可能导致某些线程(尤其是写线程)长时间饥饿。
  • 对读写锁的影响:在非公平模式下,一个释放写锁的线程,如果此时有等待的读线程和写线程,读线程通常能更快地集体获得读锁(因为允许多个读),导致等待的写线程可能延迟。公平模式则严格按照队列顺序,写线程有机会更快获得锁,但整体并发读性能可能稍低。

1.4 核心组件

Java 标准库通过 java.util.concurrent.locks.ReentrantReadWriteLock 类提供了读写锁的实现。它是 ReadWriteLock 接口的具体实现。

核心组件:

  • ReentrantReadWriteLock.ReadLock: 实现读锁的嵌套类。
  • ReentrantReadWriteLock.WriteLock: 实现写锁的嵌套类。
  • Sync (内部抽象类): 继承自 AbstractQueuedSynchronizer (AQS),是实现锁同步机制的核心。ReentrantReadWriteLock 内部维护一个 Sync 实例。

    • 状态表示 (state): Sync 使用一个 int 类型的 state 变量(32位)来同时表示读锁和写锁的状态。

      • 低 16 位 (0x0000FFFF): 表示写锁的重入次数
      • 高 16 位 (0xFFFF0000): 表示持有读锁的线程数(更精确地说,是每个获取读锁的线程持有的读锁计数之和,因为读锁可重入)。
    • 写锁获取: 检查 state 是否为 0(无锁)或低16位不为0且当前线程是写锁持有者(重入)。否则加入等待队列。

    • 读锁获取: 检查是否有写锁持有(state低16位不为0)且持有者不是当前线程(防止读锁升级死锁)。还要检查读锁数量是否溢出。在非公平模式下,如果队列头是写线程等待,新来的读线程可能被阻塞(避免写线程饥饿);公平模式则严格排队。

    • 锁释放: 相应减少 state 的高位(读)或低位(写)计数

二、使用示例

2.1 采用独占锁的姿势读、写数据

package cn.tcmeta.rwlock;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;public class Counter {private int value; // 修改的值/*** 读取操作* @param lock*/public void read(Lock lock){lock.lock();try {System.out.println(Thread.currentThread().getName() + " --> \t" +  " 正在读取数据....");try {TimeUnit.MILLISECONDS.sleep(100);System.out.println(Thread.currentThread().getName() + " --> \t" +  " 数据读取完成了... 读取到的值是: " + value);}catch (Exception e){e.printStackTrace();}}catch(Exception e){e.printStackTrace();}finally {lock.unlock();}}/*** 写操作* @param lock*/public void write(Lock lock, int newValue){lock.lock();try {System.out.println(Thread.currentThread().getName() + " --> \t" +  " 正在修改值 ... ");try {TimeUnit.MILLISECONDS.sleep(200);value = newValue;System.out.println(Thread.currentThread().getName() + " --> \t" +  " 修改完成了, 最新值是: " + value);}catch (Exception e){e.printStackTrace();}}catch(Exception e){e.printStackTrace();}finally {lock.unlock();}}
}

测试用例:

package cn.tcmeta.rwlock;import java.util.concurrent.locks.ReentrantLock;/*** @author: laoren* @description: 独占锁测试* @version: 1.0.0*/
public class T1 {public static void main(String[] args) {ReentrantLock reentrantLock = new ReentrantLock();Counter counter = new Counter();// 2个线程,执行写入操作.for (int i = 0; i < 2; i++) {int tmp = i;new Thread(() -> {counter.write(reentrantLock, tmp);}, "write-thread: ").start();}// 18个线程,执行读取操作for (int i = 0; i < 18; i++) {int temp = i;new Thread(() -> {counter.read(reentrantLock);}, "read-thread: ").start();}}
}

在这里插入图片描述

2.2 使用读写锁读、写数据

public class T2 {public static void main(String[] args) {// 创建资源类对象Counter counter = new Counter();// 使用独占锁的姿势读、写操作ReentrantLock reentrantLock = new ReentrantLock();ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();// 获取读锁ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();// 获取写锁ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();// 2个线程,执行写入操作.for (int i = 0; i < 2; i++) {int tmp = i;new Thread(() -> {counter.write(writeLock, tmp);}, "write-thread: ").start();}// 18个线程,执行读取操作for (int i = 0; i < 18; i++) {int temp = i;new Thread(() -> {counter.read(readLock);}, "read-thread: ").start();}}
}

在这里插入图片描述
经过测试可以发现, 使用读写锁的时候, 因为读是共享的,所以效率较快.写是独享的

2.3 锁降级 (Lock Downgrading)

  • 这是读写锁提供的一个安全且有用的特性
  • 指一个线程先获取写锁 -> 再获取读锁 -> 然后释放写锁的过程。
  • 目的:在持有写锁修改数据后,不立即释放所有锁,而是先获取读锁(因为持有写锁时获取读锁总是成功的),再释放写锁。这样该线程在后续读取操作时仍然持有读锁(保证了读取自己修改后数据的可见性),同时允许其他读线程并发访问(因为写锁已释放)。
  • 关键点:降级过程(获取读锁 -> 释放写锁)在持有写锁的线程内部完成,是原子性的。其他线程无法在降级过程中插入获取写锁,保证了数据在降级后对其他读线程可见时的一致性。

示例流程【伪代码】:

writeLock.lock(); // 1. 获取写锁 (独占)
try {// ... 修改共享数据 ...readLock.lock(); // 2. 在释放写锁前先获取读锁 (锁降级开始,此时写锁未释放,读锁一定成功)
} finally {writeLock.unlock(); // 3. 释放写锁 (锁降级完成,现在只持有读锁)
}
try {// ... 读取刚刚修改的数据 (其他读线程此时也可以并发读取了) ...
} finally {readLock.unlock(); // 4. 释放读锁
}

示例代码:

package cn.tcmeta.rwlock;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;/*** 锁降级*/
public class LockDowngradingDemo {private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();private static int shareCount; // 共享变量public static void main(String[] args) {Thread writeThread = new Thread(() -> {LOCK.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + " --> \t" + " 开始更新共享变量");shareCount++;try {TimeUnit.MILLISECONDS.sleep(100);} catch (Exception e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " --> \t" + " 更新完成的共享变量的值是: " + shareCount);// 在持有写锁的情况下, 获取读锁LOCK.readLock().lock();} catch (Exception e) {e.printStackTrace();} finally {// 释放写锁LOCK.writeLock().unlock();}// 进行读操作try {System.out.println(Thread.currentThread().getName() + " --> \t" + " 值是: " + shareCount);} finally {LOCK.readLock().unlock();}}, "write-thread:");Thread readThread = new Thread(() -> {LOCK.readLock().lock();try {System.out.println(Thread.currentThread().getName() + " --> \t" + " 当前的值是: " + shareCount);} catch (Exception e) {e.printStackTrace();} finally {LOCK.readLock().unlock();}}, "read-thread:");writeThread.start();try {TimeUnit.MILLISECONDS.sleep(3000);} catch (Exception e) {e.printStackTrace();}readThread.start();}
}

在这里插入图片描述

写线程无获取写锁,然后更新共享资源的值,并通过获取读锁实现降级.读线程直接获取读锁进行读操作.

三、应用场景

读写锁在Java并发编程中扮演着重要角色,特别适用于读多写少的场景。

3.1 缓存系统【高频读、低频更新】

package cn.tcmeta.rwlock;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.*;public class Cache<K, V> {private final Map<K, V> cacheMap = new HashMap<>();private final ReadWriteLock rwLock = new ReentrantReadWriteLock();private final Lock readLock = rwLock.readLock();private final Lock writeLock = rwLock.writeLock();// 获取缓存数据(多个线程可同时读取)public V get(K key) {readLock.lock();try {return cacheMap.get(key);} finally {readLock.unlock();}}// 更新缓存数据(独占写入)public void put(K key, V value) {writeLock.lock();try {cacheMap.put(key, value);} finally {writeLock.unlock();}}// 按需加载(经典读写锁应用)public V getOrLoad(K key, DataLoader<K, V> loader) {V value;// 先尝试读取readLock.lock();try {value = cacheMap.get(key);if (value != null) {return value;}} finally {readLock.unlock();}// 未找到数据,获取写锁加载writeLock.lock();try {// 双重检查(避免重复加载)value = cacheMap.get(key);if (value == null) {value = loader.load(key);cacheMap.put(key, value);}return value;} finally {writeLock.unlock();}}public interface DataLoader<K, V> {V load(K key);}
}

3.2 配置中心【配置读取多、更新少】

package cn.tcmeta.rwlock;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ConfigCenter {private volatile Map<String, String> config = new HashMap<>();private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true); // 公平锁// 获取配置值public String getConfig(String key) {rwLock.readLock().lock();try {return config.get(key);} finally {rwLock.readLock().unlock();}}// 热更新配置public void updateConfig(Map<String, String> newConfig) {rwLock.writeLock().lock();try {Map<String, String> updated = new HashMap<>(config);updated.putAll(newConfig);config = updated; // volatile保证可见性} finally {rwLock.writeLock().unlock();}}
}

3.3 金融交易系统【账户查询多、转账少】

public class AccountService {private final Map<Long, Account> accounts = new ConcurrentHashMap<>();private final ReadWriteLock rwLock = new ReentrantReadWriteLock();// 查询余额(高并发)public BigDecimal getBalance(long accountId) {rwLock.readLock().lock();try {Account acc = accounts.get(accountId);return acc != null ? acc.getBalance() : BigDecimal.ZERO;} finally {rwLock.readLock().unlock();}}// 转账操作(需要独占)public void transfer(long from, long to, BigDecimal amount) {rwLock.writeLock().lock();try {Account source = accounts.get(from);Account target = accounts.get(to);if (source.getBalance().compareTo(amount) < 0) {throw new InsufficientFundsException();}source.withdraw(amount);target.deposit(amount);} finally {rwLock.writeLock().unlock();}}
}

3.4 实时数据看板【数据读取多、更新少】

public class DashboardData {private volatile DataSnapshot currentSnapshot;private final ReadWriteLock rwLock = new ReentrantReadWriteLock();// 获取当前数据快照public DataSnapshot getCurrentData() {rwLock.readLock().lock();try {return currentSnapshot;} finally {rwLock.readLock().unlock();}}// 更新数据(使用锁降级保证数据一致性)public void updateData(DataProcessor processor) {rwLock.writeLock().lock();try {// 处理数据(独占写权限)DataSnapshot newSnapshot = processor.process(currentSnapshot);// 锁降级开始rwLock.readLock().lock();try {currentSnapshot = newSnapshot;} finally {rwLock.writeLock().unlock(); // 释放写锁,保留读锁}// 此时其他读线程可以访问新数据logChanges(newSnapshot);} finally {rwLock.readLock().unlock();}}
}

四、场景分析图表

4.1 读写锁在缓存系统的工作流程

4.2 锁降级过程图解

在这里插入图片描述

4.3 不同场景下锁的选择

在这里插入图片描述

五、最佳实践与注意事项

5.1 锁选择原则

  • 读写比例 > 10:1:优先考虑读写锁
  • 读写比例 < 3:1:使用互斥锁更高效
  • 超高频读取:考虑StampedLock的乐观读

5.2 避免常见陷阱

// 错误示例:在读锁保护下修改数据
public void unsafeIncrement() {readLock.lock();try {counter++; // 危险操作!} finally {readLock.unlock();}
}// 正确做法:写锁保护写操作
public void safeIncrement() {writeLock.lock();try {counter++;} finally {writeLock.unlock();}
}

5.3 性能调优技巧

// 1. 使用tryLock避免长时间阻塞
if (writeLock.tryLock(100, TimeUnit.MILLISECONDS)) {try {// 关键操作} finally {writeLock.unlock();}
}// 2. 公平锁防止写线程饥饿
new ReentrantReadWriteLock(true); // 创建公平锁// 3. 监控锁竞争情况
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
System.out.println("读队列长度: " + lock.getReadLockCount());
System.out.println("写队列长度: " + lock.getQueueLength());

5.4 替代解决方案

场景推荐方案优点缺点
读多写少ReentrantReadWriteLock成熟稳定,支持锁降级写线程可能饥饿
超高并发读StampedLock乐观读性能极高API复杂,不可重入
读写均衡ReentrantLock简单高效读操作无法并发
数据快照CopyOnWriteArrayList读完全无锁写开销大,内存占用高

六、典型应用场景总结

  1. 数据检索系统
    • 场景:商品目录、用户资料查询
    • 特点:95%读操作,5%数据更新
    • 实现:读写锁保护核心数据集合
  2. 实时监控系统
    • 场景:服务器监控、交易大盘
    • 特点:高频读取,周期性数据刷新
    • 实现:锁降级保证数据更新一致性
  3. 多级缓存同步
    • 场景:本地缓存与中央缓存同步
    • 特点:本地读为主,偶尔批量更新
    • 实现:写锁保护缓存更新过程
  4. 配置管理系统
    • 场景:微服务配置中心
    • 特点:服务启动时密集读取,偶尔热更新
    • 实现:读写分离保证高可用性

读写锁在Java并发工具箱中是一个强大的工具,合理使用可以显著提升系统吞吐量。但必须注意其适用边界——在真正的"读多写少"场景中才能发挥最大价值。对于临界区短小的操作,或写操作频繁的场景,传统的互斥锁可能是更简单高效的选择。

七、没有了

学习愉快!


资料内容:
链接: https://pan.baidu.com/s/1_igGW3DT7pGsNJPMoOpV6g 提取码: r35a
如资料失效,请评论区留言或者留邮箱. 🎁

在这里插入图片描述

http://www.lryc.cn/news/624988.html

相关文章:

  • 宁波市第八届网络安全大赛初赛(REVERSE-Writeup)
  • 基于Spring Boot+Vue的社区便民服务平台 智慧社区平台 志愿者服务管理
  • day25|学习前端js
  • Product Hunt 每日热榜 | 2025-08-18
  • 【yocto】为什么要选择yocto?
  • 亚马逊新手突围:从流量破冰到持续出单
  • Less (CSS 预处理器)
  • 问答社区运营优化:cpolar 提升 Answer 平台远程访问速度方案
  • 性能测试(Jemter)
  • day44_2025-08-18
  • PMP-项目管理-十大知识领域:风险管理-识别、评估、应对项目风险
  • 兴趣爱好——虾哥开源小智AI机器人搭建(丐版—最低成本)ESP32开发板 MicroPython V1.0.0 Rev1
  • 继承中的向上转型、向下转型与动态绑定的深入解析
  • 学习游戏制作记录(各种独特物品效果)8.18
  • 【Langchain系列二】LangChain+Prompt +LLM智能问答入门
  • Prompt engineering(PE) —— prompt 优化如何进行?
  • 集成电路学习:什么是Face Detection人脸检测
  • leetcode4_452 and 763
  • 【论文学习】UoMo: 一个用于无线网络优化的移动流量预测通用模型
  • 学习嵌入式的第二十天——数据结构
  • 如何解决机器翻译的“幻觉“问题(Hallucination)?
  • 特赞内容运营解决方案,AI重构品牌内容价值链
  • (Arxiv-2025)OPENS2V-NEXUS:一个面向主体到视频生成的详细基准与百万规模数据集
  • 知识蒸馏 - 各类概率分布
  • 概率论基础教程第4章 随机变量(三)
  • 基于 Ansible 与 Jinja2 模板的 LNMP 环境及 WordPress 自动化部署实践
  • 数据库:表和索引结构
  • 【68页PPT】智慧方案信息物理系统与数字孪生某著名企业(附下载方式)
  • 从零开始搭建React+TypeScript+webpack开发环境——多环境配置管理方案
  • 图论:Floyd算法