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.mfbz.cn/a/2798.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

docker compose再阿里云上无法使用的问题

最原始的Dokcerfile # 使用官方Python 3.6.8镜像 FROM python:3.6.8-slimWORKDIR /app# 复制依赖文件 COPY requirements.txt .RUN pip install --upgrade pip # 检查并安装依赖&#xff08;自动处理未安装的包&#xff09; RUN pip install --no-cache-dir -r requirements.tx…

【运维进阶】LNMP + WordPress 自动化部署实验

LNMP WordPress 自动化部署实验 一、实验目标 通过 Ansible 自动化工具&#xff0c;在目标服务器&#xff08;lnmp 主机组&#xff09;上搭建 LNMP 架构&#xff08;Linux 系统 Nginx 网页服务器 MariaDB 数据库 PHP 脚本语言&#xff09;&#xff0c;并部署 WordPress 博…

豆包 Java的23种设计模式

Java的23种设计模式是软件开发中常用的设计思想总结&#xff0c;根据用途可分为三大类&#xff1a;创建型、结构型和行为型。 一、创建型模式&#xff08;5种&#xff09; 用于处理对象创建机制&#xff0c;隐藏创建逻辑&#xff0c;使程序更灵活。 单例模式&#xff1a;保证一…

RISC-V汇编新手入门

有空就更。一、基础核心概念&#xff1a;什么是汇编语言&#xff1f;汇编语言是直接对应 CPU 指令的低级编程语言&#xff0c;每一行汇编代码基本对应一条 CPU 能直接执行的指令。相比 C 语言等高级语言&#xff0c;汇编更贴近硬件&#xff0c;能直接操作 CPU 的寄存器、内存和…

[每周一更]-(第155期):Go 1.25 发布:新特性、技术思考与 Go vs Rust 竞争格局分析

作为一名 Go 研发工程师&#xff0c;我一直关注 Go 语言的演进。2025 年 8 月 12 日&#xff0c;Go 团队发布了 Go 1.25 版本&#xff0c;这是继 Go 1.24 之后的又一重要更新。 这个版本聚焦于工具链优化、运行时改进和实验性功能引入&#xff0c;没有引入破坏性语言变化&#…

【网络安全】Webshell的绕过——绕过动态检测引擎WAF-缓存绕过(Hash碰撞)

目录 一、前言 二、环境 三、了解动态检测引擎 3.1 shuffle — 打乱数组 3.2 mt_srand — 播下一个更好的随机数发生器种子 四、缓存导致的绕过【hash碰撞】 五、总结 一、前言 在渗透测试过程中&#xff0c;成功获取 WebShell 时难免遇到 Web 应用防火墙&#xff08;WA…

【Linux | 网络】高级IO

一、IO是什么二、五种IO模型2.1 理解五种IO模型2.2 五种IO模型的定义三、 非阻塞IO3.1 fcntl函数3.2 实现函数SetNoBlock&#xff08;将文件描述符设置为非阻塞&#xff09;四、多路转接IO4.1 多路转接IO之select4.1.1 select函数4.1.2 select的优缺点4.2 多路转接IO之poll4.2.…

图解简单选择排序C语言实现

1 简单选择排序 简单选择排序&#xff08;Simple Selection Sort&#xff09;是一种基础且直观的排序算法&#xff0c;其核心思想是通过重复选择未排序部分中的最小&#xff08;或最大&#xff09;元素&#xff0c;并将其放到已排序部分的末尾&#xff0c;逐步完成整个序列的排…

[go] 桥接模式

桥接模式 是一种结构型设计模式&#xff0c; 可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构&#xff0c; 从而能在开发时分别使用。 模型说明抽象部分&#xff08;Abstraction&#xff09;提供高层控制逻辑&#xff0c;依赖于完成底层实际工作的实现对象…

【自用】JavaSE--特殊文件Properties与XML、日志技术

特殊文件概述使用特殊文件可以存储多个有关系的数据&#xff0c;作为系统的配置信息属性文件类似于键值对&#xff0c;一一对应存储数据(比如用户名与密码)XML文件存储多个用户的多个属性更适合&#xff0c;适合存储更复杂的数据Properties注&#xff1a;这个属性文件的后缀即使…

【轨物方案】预防性运维:轨物科技用AI+机器人重塑光伏电站价值链

传统光伏电站的运维模式&#xff0c;常常被视为一个“成本中心”&#xff0c;其“故障-抢修”的逻辑模式&#xff0c;不仅响应滞后、效率低下&#xff0c;更难以从根本上提升资产的长期价值。然而&#xff0c;随着新能源行业的深刻发展&#xff0c;运维的价值被重新定义&#x…

【自动化运维神器Ansible】Ansible比较操作符详解:从基础到实战应用

目录 引言 1 Ansible比较操作符概述 1.1 什么是比较操作符&#xff1f; 1.2 比较操作符的分类与核心符号 2 核心比较操作符详解 2.1 相等与不等&#xff1a;与! 语法与基础用法 示例&#xff1a;字符串与数值比较 注意事项 2.2 大小比较&#xff1a;>、>、<…