一、读写锁概述

Java 中读写锁(ReadWriteLock)的核心概念。读写锁是 Java 并发包(java.util.concurrent.locks)中解决特定并发场景下性能问题的重要工具。
1.1 核心目标
提高【读多写少】场景下的并发性能
想象一个共享资源(比如一个配置字典、一个缓存或一个大型数据集):
- 读操作(
read):通常不会修改数据,只是获取数据。多个线程同时读取同一个数据通常是安全的。 - 写操作(
write):会修改数据。为了保证数据一致性,写操作通常需要独占访问资源。即写操作进行时,不允许其他任何线程(读或写)访问资源。
传统的互斥锁(如 synchronized 或 ReentrantLock)在访问共享资源时,无论读写,都只允许一个线程访问。这在“读多写少”的场景下会造成巨大的性能瓶颈:大量只读线程被强制串行执行,即使它们之间本可以安全地并发读取
1.2 核心思想
读写锁的核心思想:分离读锁和写锁
读写锁巧妙地解决了这个问题,它维护了一对锁
-
读锁(共享锁,
read lock)- 共享性:多个线程可以同时持有读锁。
- 目的:允许多个线程并发地读取共享资源,极大地提升读取的吞吐量。
- 约束:当一个(或多个)线程持有读锁时,任何线程都无法获取写锁。这是为了保证读取数据的一致性——防止在读取过程中数据被修改。
-
写锁(独占锁,
write lock-
排他性:同一时刻只能有一个线程持有写锁。
-
目的:保证写操作的原子性和数据一致性。写操作需要独占访问资源。
-
约束:
- 当一个线程持有写锁时,其他任何线程(无论是想读还是想写)都无法获取读锁或写锁。
- 在获取写锁之前,必须等待所有已持有的读锁释放。同样,在获取读锁之前,必须等待已持有的写锁释放
-
1.3 关键规则与保证
读写锁的行为严格遵循以下规则,这些规则是理解其并发语义的基础:
-
读读共享(Read-Read Sharing):多个线程可以同时获取并持有读锁,进行并发读取操作。这是性能提升的关键。
-
读写互斥(Read-Write Mutual Exclusion):
- 如果一个线程持有读锁,另一个线程尝试获取写锁会被阻塞,直到所有读锁释放。
- 如果一个线程持有写锁,另一个线程尝试获取读锁会被阻塞,直到写锁释放。
-
写写互斥(Write-Write Mutual Exclusion):同一时刻只能有一个线程持有写锁。尝试获取写锁的线程会被阻塞,直到当前写锁释放。
-
可重入性(Reentrancy):
- Java 的标准实现
ReentrantReadWriteLock支持锁的可重入。 - 一个线程可以重复获取它已经持有的读锁或写锁(需要相应次数的释放)。
- 持有写锁的线程可以再次获取读锁(锁降级的关键)。
- 持有读锁的线程不能直接获取写锁(尝试获取写锁会阻塞,可能导致死锁)。如果需要升级,必须先释放所有读锁,再尝试获取写锁,但这个操作不是原子的,中间状态可能被其他写线程抢占。因此,锁升级通常不被推荐,且标准实现不支持。
- Java 的标准实现
-
公平性(Fairness):
- 类似于
ReentrantLock,ReentrantReadWriteLock可以构造为公平锁或非公平锁(默认)。 - 公平锁:线程按照请求锁的顺序(近似 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 | 读完全无锁 | 写开销大,内存占用高 |
六、典型应用场景总结
- 数据检索系统:
- 场景:商品目录、用户资料查询
- 特点:95%读操作,5%数据更新
- 实现:读写锁保护核心数据集合
- 实时监控系统:
- 场景:服务器监控、交易大盘
- 特点:高频读取,周期性数据刷新
- 实现:锁降级保证数据更新一致性
- 多级缓存同步:
- 场景:本地缓存与中央缓存同步
- 特点:本地读为主,偶尔批量更新
- 实现:写锁保护缓存更新过程
- 配置管理系统:
- 场景:微服务配置中心
- 特点:服务启动时密集读取,偶尔热更新
- 实现:读写分离保证高可用性
读写锁在Java并发工具箱中是一个强大的工具,合理使用可以显著提升系统吞吐量。但必须注意其适用边界——在真正的"读多写少"场景中才能发挥最大价值。对于临界区短小的操作,或写操作频繁的场景,传统的互斥锁可能是更简单高效的选择。
七、没有了
学习愉快!
资料内容:
链接: https://pan.baidu.com/s/1_igGW3DT7pGsNJPMoOpV6g 提取码: r35a
如资料失效,请评论区留言或者留邮箱. 🎁

691

被折叠的 条评论
为什么被折叠?



