第6章
🔒 读写锁
掌握读写分离的高效并发控制机制,理解ReentrantReadWriteLock和StampedLock的原理与应用
学习目标
- 理解读写锁的设计原理和适用场景
- 掌握ReentrantReadWriteLock的使用方法
- 了解锁的升级和降级机制
- 学习StampedLock的高级特性
- 进行读写锁的性能分析和优化
读写锁概述
读写锁(ReadWriteLock)是一种特殊的锁机制,它允许多个线程同时获取读锁,但只允许一个线程获取写锁。这种设计基于一个重要的观察:在很多应用场景中,读操作远比写操作频繁,而多个读操作之间并不会产生数据竞争。
核心思想
读写锁通过读写分离的策略,在保证数据一致性的前提下,显著提高了并发读取的性能。
读写锁的特性
共享读锁
多个线程可以同时获取读锁,实现并发读取,提高系统吞吐量。
独占写锁
只有一个线程可以获取写锁,确保写操作的原子性和数据一致性。
互斥性
读锁和写锁之间是互斥的,写锁和写锁之间也是互斥的。
适用场景
- 缓存系统:频繁的读取操作,偶尔的更新操作
- 配置管理:配置信息读取频繁,修改较少
- 统计数据:数据查询多,数据更新少
- 资源池:资源获取频繁,资源管理操作较少
ReentrantReadWriteLock详解
ReentrantReadWriteLock是Java中ReadWriteLock接口的标准实现,它提供了可重入的读写锁功能。该类内部维护了两个锁:一个读锁和一个写锁,它们共享同一个同步状态。
基本用法
ReentrantReadWriteLock基本示例
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private String data = "初始数据";
// 读操作
public String readData() {
readLock.lock();
try {
// 模拟读取操作
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 读取数据: " + data);
return data;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
readLock.unlock();
}
}
// 写操作
public void writeData(String newData) {
writeLock.lock();
try {
// 模拟写入操作
Thread.sleep(2000);
this.data = newData;
System.out.println(Thread.currentThread().getName() + " 写入数据: " + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
writeLock.unlock();
}
}
}
锁的特性
可重入性
同一个线程可以多次获取同一个锁,避免死锁问题。
公平性
支持公平和非公平两种模式,可在构造时指定。
超时获取
支持tryLock()方法,可设置超时时间避免无限等待。
锁的升级和降级
ReentrantReadWriteLock支持锁降级但不支持锁升级。锁降级是指持有写锁的线程可以获取读锁,然后释放写锁,从而将写锁降级为读锁。
重要提醒
ReentrantReadWriteLock不支持锁升级(从读锁升级到写锁),这样设计是为了避免死锁。
锁降级示例
锁降级实现
public class LockDowngradeExample {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private volatile boolean dataChanged = false;
private String data = "初始数据";
public void processData() {
readLock.lock();
try {
if (!dataChanged) {
// 需要更新数据,先释放读锁
readLock.unlock();
// 获取写锁
writeLock.lock();
try {
// 双重检查
if (!dataChanged) {
data = "更新后的数据";
dataChanged = true;
}
// 锁降级:在释放写锁之前获取读锁
readLock.lock();
} finally {
writeLock.unlock(); // 释放写锁
}
}
// 使用数据(持有读锁)
System.out.println("处理数据: " + data);
} finally {
readLock.unlock();
}
}
}
锁降级的优势
- 数据一致性:确保在数据更新后立即读取到最新数据
- 性能优化:避免释放写锁后其他线程修改数据
- 原子操作:保证更新和读取操作的原子性
StampedLock高级特性
StampedLock是Java 8引入的新锁机制,它提供了三种锁模式:写锁、悲观读锁和乐观读。相比ReentrantReadWriteLock,StampedLock在读多写少的场景下性能更优。
三种锁模式
写锁
独占锁,与ReentrantReadWriteLock的写锁类似,但不可重入。
悲观读锁
共享锁,与ReentrantReadWriteLock的读锁类似,但不可重入。
乐观读
无锁操作,通过版本号检查数据是否被修改,性能最优。
StampedLock使用示例
StampedLock完整示例
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private final StampedLock lock = new StampedLock();
private double x, y;
// 写操作
public void write(double newX, double newY) {
long stamp = lock.writeLock();
try {
x = newX;
y = newY;
} finally {
lock.unlockWrite(stamp);
}
}
// 乐观读
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double curX = x, 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[] getCurrentPoint() {
long stamp = lock.readLock();
try {
return new double[]{x, y};
} finally {
lock.unlockRead(stamp);
}
}
// 读锁升级为写锁
public void moveIfAtOrigin(double newX, double newY) {
long stamp = lock.readLock();
try {
while (x == 0.0 && y == 0.0) {
// 尝试将读锁升级为写锁
long writeStamp = lock.tryConvertToWriteLock(stamp);
if (writeStamp != 0L) {
stamp = writeStamp;
x = newX;
y = newY;
break;
} else {
// 升级失败,释放读锁,获取写锁
lock.unlockRead(stamp);
stamp = lock.writeLock();
}
}
} finally {
lock.unlock(stamp);
}
}
}
StampedLock的优势
- 更高性能:乐观读模式避免了锁竞争,提高并发性能
- 锁转换:支持锁模式之间的转换,提供更大的灵活性
- 无饥饿:写锁不会被读锁无限期阻塞
- 内存效率:相比ReentrantReadWriteLock占用更少内存
性能分析与最佳实践
选择合适的读写锁实现对系统性能有重要影响。不同的锁机制在不同场景下表现各异,需要根据具体的应用特点进行选择。
性能对比
读多写少场景
StampedLock > ReentrantReadWriteLock > synchronized
读写均衡场景
ReentrantReadWriteLock ≈ StampedLock > synchronized
写多读少场景
synchronized ≈ ReentrantReadWriteLock ≈ StampedLock
最佳实践
使用建议
- 场景选择:读多写少场景优先考虑StampedLock
- 可重入需求:需要可重入特性时选择ReentrantReadWriteLock
- 公平性要求:需要公平锁时使用ReentrantReadWriteLock
- 简单场景:简单的互斥场景可以使用synchronized
注意事项
- 避免锁升级:ReentrantReadWriteLock不支持从读锁升级到写锁
- 正确释放锁:使用try-finally确保锁的正确释放
- 避免长时间持锁:减少锁的持有时间,提高并发性能
- 合理使用乐观读:在StampedLock中合理使用乐观读模式
- 监控锁竞争:定期监控锁的竞争情况,及时优化