Java并发-读写锁ReadWriteLock
在并发编程中,我们经常遇到读多写少的场景。比如缓存系统、配置管理、统计计数器等,这些场景下读操作的频率远远超过写操作。如果简单地用 synchronized
或 ReentrantLock
来处理,虽然能保证线程安全,但性能会受到很大影响。
举个例子,假设我们有一个简单的数据容器:
public static class SimpleContainer {
private int value;
public synchronized int read() throws InterruptedException {
// 模拟读操作耗时
Thread.sleep(1000);
return value;
}
public synchronized void write(int newValue) throws InterruptedException {
// 模拟写操作耗时
Thread.sleep(1000);
this.value = newValue;
}
}
这种实现虽然简单,但存在一个明显的问题:即使是多个线程同时读取数据(理论上不会产生数据竞争),它们也必须排队等待,无法并发执行。这就是读写锁要解决的核心问题。
# 一、什么是读写锁?
读写锁(ReadWriteLock
)是一种特殊的锁机制,它将对共享资源的访问分为两种类型:
- 读锁(共享锁):多个线程可以同时持有读锁,互不冲突
- 写锁(独占锁):只有一个线程可以持有写锁,且与读锁互斥
这种设计的核心思想是:读读不冲突,读写冲突,写写冲突。
# 二、实现
Java 提供了 ReentrantReadWriteLock
作为 ReadWriteLock
接口的标准实现。让我们看看如何使用它:
public static class ReadWriteContainer {
private int value;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public int read() throws InterruptedException {
readLock.lock();
try {
// 模拟读操作耗时
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 正在读取: " + value);
return value;
} finally {
readLock.unlock();
}
}
public void write(int newValue) throws InterruptedException {
writeLock.lock();
try {
// 模拟写操作耗时
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 正在写入: " + newValue);
this.value = newValue;
} finally {
writeLock.unlock();
}
}
}
使用示例:
public static void main(String[] args) {
ReadWriteContainer container = new ReadWriteContainer();
// 启动 2 个写线程
for (int i = 0; i < 2; i++) {
final int writeValue = i + 1;
new Thread(() -> {
try {
container.write(writeValue);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Writer-" + i).start();
}
// 启动 5 个读线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
container.read();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Reader-" + i).start();
}
}
运行这段代码,你会发现:
- 多个读线程可以并发执行,不需要相互等待
- 写线程会独占访问,其他所有操作都被阻塞
- 读写之间互斥,有写操作时读操作会等待,反之亦然
这样的设计在读多写少的场景下,性能提升是非常明显的。想象一下,如果有 100 个读请求和 1 个写请求,传统互斥锁需要顺序执行 101 次操作,而读写锁允许 100 个读操作并发执行,只有写操作需要独占。
# 三、读写锁的关键特性
- 公平性选择:
ReentrantReadWriteLock
支持公平和非公平模式 - 重入性:同一线程可以多次获取读锁或写锁
- 锁降级:持有写锁的线程可以获取读锁,然后释放写锁(但不支持锁升级)
- 条件变量:写锁支持条件变量,读锁不支持
# 四、StampedLock:更高性能的选择
Java 8 引入了 StampedLock
,这是对传统读写锁的重要改进。传统读写锁虽然允许多个读操作并发,但读写之间依然是互斥的。StampedLock
通过引入乐观读的概念,进一步提升了性能。
# 1、的三种锁模式
- 写锁:独占锁,与传统读写锁的写锁类似
- 悲观读锁:共享锁,与传统读写锁的读锁类似
- 乐观读:这是
StampedLock
的核心创新,不是真正的锁
# 2、实际应用示例
public class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
// 写操作:使用独占锁
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
// 读操作:先尝试乐观读,失败则降级为悲观读
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // 尝试乐观读
// 在乐观读状态下读取数据
double curX = x;
double curY = y;
// 检查期间是否有写操作
if (!lock.validate(stamp)) {
// 乐观读失败,升级为悲观读锁
stamp = lock.readLock();
try {
curX = x;
curY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(curX * curX + curY * curY);
}
// 只读操作的另一种实现方式
public double getX() {
long stamp = lock.tryOptimisticRead();
double currentX = x;
// 如果期间没有写操作,直接返回
if (lock.validate(stamp)) {
return currentX;
}
// 否则使用悲观读锁
stamp = lock.readLock();
try {
return x;
} finally {
lock.unlockRead(stamp);
}
}
}
# 3、乐观读的工作原理
- 获取戳记:通过
tryOptimisticRead()
获取一个stamp
(戳记) - 读取数据:在没有锁保护的情况下读取共享变量
- 验证戳记:通过
validate(stamp)
检查期间是否有写操作 - 处理结果:
- 如果验证成功,说明读取的数据是有效的
- 如果验证失败,需要重新读取或升级为悲观锁
# 4、性能优势
StampedLock
的乐观读机制带来了显著的性能提升:
- 无锁开销:乐观读不需要获取实际的锁,避免了锁竞争
- 更好的并发性:读操作不会阻塞写操作的获取
- 适应性强:在读多写少的场景下,大部分读操作都能通过乐观读完成
但要注意,StampedLock
也有一些限制:
- 不支持重入
- 不支持条件变量
- 使用复杂度相对较高
# 五、如何选择合适的锁?
根据不同的应用场景,我们可以这样选择:
场景 | 推荐方案 | 理由 |
---|---|---|
读写比例相当 | synchronized 或 ReentrantLock | 实现简单,性能够用 |
读多写少,对性能要求不高 | ReentrantReadWriteLock | 平衡了复杂度和性能 |
读多写少,对性能要求很高 | StampedLock | 最佳性能,但实现复杂 |
需要重入或条件变量 | ReentrantReadWriteLock | StampedLock 不支持这些特性 |
总的来说,读写锁是并发编程中的重要工具,正确选择和使用它们,能够在保证线程安全的前提下显著提升应用性能。
编辑 (opens new window)
上次更新: 2025/08/14