读写锁
面试高频考点:读写锁是一种优化锁机制,允许多个读操作同时进行,但写操作互斥。掌握读写锁的特性、使用场景和 StampedLock 是面试重点。
核心概念
什么是读写锁
读写锁是一种将锁分为读锁和写锁的机制:
- 读锁(共享锁):允许多个线程同时获取,用于读取操作
- 写锁(排他锁):同一时间只允许一个线程获取,用于写入操作
┌─────────────────────────────────────────────────────┐
│ 读写锁状态 │
├───────────────┬─────────────────────────────────────┤
│ 读读:共享 │ 线程A读取 ══╗ │
│ │ 线程B读取 ══╣→ 可同时进行 │
│ │ 线程C读取 ══╝ │
├───────────────┼─────────────────────────────────────┤
│ 读写:互斥 │ 线程A读取 ══╗ │
│ │ 线程B写入 ═╣→ 写入需等待 │
│ │ ═╝ │
├───────────────┼─────────────────────────────────────┤
│ 写写:互斥 │ 线程A写入 ══╗ │
│ │ 线程B写入 ══╣→ 互斥等待 │
│ │ ═╝ │
└───────────────┴─────────────────────────────────────┘适用场景
读写锁适用于读多写少的场景:
- 缓存系统
- 配置管理
- 共享数据统计
- 数据库查询缓存
ReentrantReadWriteLock 使用
基本用法
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作:使用读锁
public V get(K key) {
rwLock.readLock().lock();
try {
return map.get(key);
} finally {
rwLock.readLock().unlock();
}
}
// 写操作:使用写锁
public void put(K key, V value) {
rwLock.writeLock().lock();
try {
map.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
// 删除操作:使用写锁
public V remove(K key) {
rwLock.writeLock().lock();
try {
return map.remove(key);
} finally {
rwLock.writeLock().unlock();
}
}
// 清空操作:使用写锁
public void clear() {
rwLock.writeLock().lock();
try {
map.clear();
} finally {
rwLock.writeLock().unlock();
}
}
}完整缓存示例
public class ThreadSafeCache<K, V> {
private final Map<K, V> cache = 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 cache.get(key);
} finally {
readLock.unlock();
}
}
// 获取缓存,不存在则加载
public V getOrLoad(K key, Function<K, V> loader) {
// 先用读锁尝试获取
readLock.lock();
try {
V value = cache.get(key);
if (value != null) {
return value;
}
} finally {
readLock.unlock();
}
// 未命中,使用写锁加载
writeLock.lock();
try {
// 双重检查,防止其他线程已加载
V value = cache.get(key);
if (value == null) {
value = loader.apply(key);
cache.put(key, value);
}
return value;
} finally {
writeLock.unlock();
}
}
// 更新缓存
public void put(K key, V value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
// 获取缓存大小
public int size() {
readLock.lock();
try {
return cache.size();
} finally {
readLock.unlock();
}
}
// 批量获取
public Map<K, V> getAll(Collection<K> keys) {
readLock.lock();
try {
Map<K, V> result = new HashMap<>();
for (K key : keys) {
V value = cache.get(key);
if (value != null) {
result.put(key, value);
}
}
return result;
} finally {
readLock.unlock();
}
}
}读写锁特性
公平性选择
// 非公平锁(默认)- 吞吐量高
ReadWriteLock unfairLock = new ReentrantReadWriteLock();
// 公平锁 - 按请求顺序获取
ReadWriteLock fairLock = new ReentrantReadWriteLock(true);公平锁 vs 非公平锁:
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | FIFO 先进先出 | 可能插队 |
| 吞吐量 | 较低 | 较高 |
| 线程饥饿 | 不会发生 | 可能发生 |
| 适用场景 | 对顺序要求高 | 追求性能 |
可重入性
public class ReentrantExample {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读锁可重入
public void readMethod1() {
rwLock.readLock().lock();
try {
System.out.println("读方法1");
readMethod2(); // 再次获取读锁
} finally {
rwLock.readLock().unlock();
}
}
public void readMethod2() {
rwLock.readLock().lock(); // 可重入
try {
System.out.println("读方法2");
} finally {
rwLock.readLock().unlock();
}
}
// 写锁可重入
public void writeMethod1() {
rwLock.writeLock().lock();
try {
System.out.println("写方法1");
writeMethod2(); // 再次获取写锁
} finally {
rwLock.writeLock().unlock();
}
}
public void writeMethod2() {
rwLock.writeLock().lock(); // 可重入
try {
System.out.println("写方法2");
} finally {
rwLock.writeLock().unlock();
}
}
}锁降级
锁降级:从写锁降级为读锁,允许在持有写锁时获取读锁。
public class LockDowngrade {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private Object data;
// 正确的锁降级
public void processWithDowngrade() {
rwLock.writeLock().lock(); // 获取写锁
try {
// 更新数据
data = new Object();
// 获取读锁(在写锁持有期间)
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock(); // 释放写锁,此时仍持有读锁
}
// 此时只有读锁,其他读线程可以并发访问
try {
// 继续读取操作
System.out.println(data);
} finally {
rwLock.readLock().unlock(); // 最后释放读锁
}
}
}锁升级(不支持)
// 错误示例:锁升级会导致死锁
public void wrongUpgrade() {
rwLock.readLock().lock(); // 获取读锁
try {
// 尝试获取写锁 - 死锁!
rwLock.writeLock().lock(); // 永远无法获取
} finally {
rwLock.readLock().unlock();
}
}
// 正确做法:先释放读锁,再获取写锁
public void correctUpgrade() {
rwLock.readLock().lock();
try {
// 读取检查
if (needUpdate) {
rwLock.readLock().unlock(); // 先释放读锁
rwLock.writeLock().lock(); // 再获取写锁
try {
// 双重检查
if (needUpdate) {
// 更新操作
}
} finally {
rwLock.writeLock().unlock();
}
rwLock.readLock().lock(); // 再次获取读锁(如果需要继续读)
}
} finally {
rwLock.readLock().unlock();
}
}StampedLock 简介
什么是 StampedLock
StampedLock 是 Java 8 引入的一种新型锁,相比 ReentrantReadWriteLock:
- 性能更好:使用乐观读锁,读操作完全不加锁
- 不可重入:使用时需要小心死锁
- 返回戳记:锁的获取和释放需要配合 stamp
基本用法
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private final StampedLock sl = new StampedLock();
private double x, y;
// 写锁
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 释放写锁
}
}
// 悲观读锁
public double distanceFromOrigin() {
long stamp = sl.readLock(); // 获取读锁
try {
return Math.sqrt(x * x + y * y);
} finally {
sl.unlockRead(stamp); // 释放读锁
}
}
// 乐观读锁
public double distanceFromOriginOptimistic() {
long stamp = sl.tryOptimisticRead(); // 获取乐观读戳记
double currentX = x, currentY = y; // 读取数据
if (!sl.validate(stamp)) { // 检查是否有写操作
// 有写操作,乐观读失败,升级为悲观读
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}乐观读锁
工作原理
乐观读流程:
┌──────────────────────────────────────────────────────┐
│ 1. tryOptimisticRead() 获取戳记 │
│ ↓ │
│ 2. 读取数据(不加锁,可能读到不一致数据) │
│ ↓ │
│ 3. validate(stamp) 检查戳记是否有效 │
│ ├─ 有效 → 使用读取的数据 │
│ └─ 无效 → 升级为悲观读锁,重新读取 │
└──────────────────────────────────────────────────────┘乐观读锁示例
public class OptimisticCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final StampedLock sl = new StampedLock();
// 乐观读获取
public V get(K key) {
long stamp = sl.tryOptimisticRead(); // 乐观读
V value = cache.get(key); // 读取数据(可能不一致)
if (!sl.validate(stamp)) { // 验证是否有效
// 无效,升级为悲观读
stamp = sl.readLock();
try {
value = cache.get(key); // 重新读取
} finally {
sl.unlockRead(stamp);
}
}
return value;
}
// 乐观读获取,带默认值
public V getOrDefault(K key, V defaultValue) {
long stamp = sl.tryOptimisticRead();
V value = cache.get(key);
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
value = cache.get(key);
} finally {
sl.unlockRead(stamp);
}
}
return value != null ? value : defaultValue;
}
// 写操作
public void put(K key, V value) {
long stamp = sl.writeLock();
try {
cache.put(key, value);
} finally {
sl.unlockWrite(stamp);
}
}
}锁转换
public class LockConversion {
private final StampedLock sl = new StampedLock();
private double value;
// 乐观读转悲观读
public void optimisticToPessimistic() {
long stamp = sl.tryOptimisticRead();
double v = value;
if (!sl.validate(stamp)) {
stamp = sl.readLock(); // 转换为悲观读
try {
v = value;
} finally {
sl.unlockRead(stamp);
}
}
}
// 读锁转写锁
public void readToWrite() {
long stamp = sl.readLock();
try {
while (true) {
long ws = sl.tryConvertToWriteLock(stamp); // 尝试转换
if (ws != 0) { // 转换成功
stamp = ws;
// 执行写操作
break;
} else { // 转换失败
sl.unlockRead(stamp);
stamp = sl.writeLock(); // 直接获取写锁
}
}
} finally {
sl.unlockWrite(stamp);
}
}
}性能对比
基准测试
public class LockBenchmark {
private static final int THREAD_COUNT = 10;
private static final int ITERATIONS = 1000000;
// ReentrantLock 性能测试
public static void testReentrantLock() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
AtomicInteger counter = new AtomicInteger();
long start = System.currentTimeMillis();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
lock.lock();
try {
counter.incrementAndGet();
} finally {
lock.unlock();
}
}
});
}
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
System.out.println("ReentrantLock: " + (System.currentTimeMillis() - start) + "ms");
}
// ReentrantReadWriteLock 性能测试(读多写少)
public static void testReadWriteLock() throws InterruptedException {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
AtomicInteger counter = new AtomicInteger();
long start = System.currentTimeMillis();
Thread[] readers = new Thread[THREAD_COUNT - 1];
for (int i = 0; i < readers.length; i++) {
readers[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
rwLock.readLock().lock();
try {
counter.get();
} finally {
rwLock.readLock().unlock();
}
}
});
}
Thread writer = new Thread(() -> {
for (int j = 0; j < ITERATIONS / 10; j++) { // 写操作少
rwLock.writeLock().lock();
try {
counter.incrementAndGet();
} finally {
rwLock.writeLock().unlock();
}
}
});
for (Thread t : readers) t.start();
writer.start();
for (Thread t : readers) t.join();
writer.join();
System.out.println("ReadWriteLock: " + (System.currentTimeMillis() - start) + "ms");
}
// StampedLock 性能测试(乐观读)
public static void testStampedLock() throws InterruptedException {
StampedLock sl = new StampedLock();
AtomicInteger counter = new AtomicInteger();
long start = System.currentTimeMillis();
Thread[] readers = new Thread[THREAD_COUNT - 1];
for (int i = 0; i < readers.length; i++) {
readers[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
long stamp = sl.tryOptimisticRead();
counter.get();
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
counter.get();
} finally {
sl.unlockRead(stamp);
}
}
}
});
}
Thread writer = new Thread(() -> {
for (int j = 0; j < ITERATIONS / 10; j++) {
long stamp = sl.writeLock();
try {
counter.incrementAndGet();
} finally {
sl.unlockWrite(stamp);
}
}
});
for (Thread t : readers) t.start();
writer.start();
for (Thread t : readers) t.join();
writer.join();
System.out.println("StampedLock: " + (System.currentTimeMillis() - start) + "ms");
}
}性能对比结果
| 场景 | ReentrantLock | ReentrantReadWriteLock | StampedLock |
|---|---|---|---|
| 纯读 | 慢(互斥) | 快(共享) | 最快(乐观读) |
| 纯写 | 快 | 慢 | 中等 |
| 读多写少 | 慢 | 快 | 最快 |
| 写多读少 | 中等 | 慢 | 慢 |
实际应用示例
线程安全的计数器
public class ThreadSafeCounter {
private final StampedLock sl = new StampedLock();
private long value;
public void increment() {
long stamp = sl.writeLock();
try {
value++;
} finally {
sl.unlockWrite(stamp);
}
}
public void decrement() {
long stamp = sl.writeLock();
try {
value--;
} finally {
sl.unlockWrite(stamp);
}
}
public long get() {
long stamp = sl.tryOptimisticRead();
long currentValue = value;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentValue = value;
} finally {
sl.unlockRead(stamp);
}
}
return currentValue;
}
public long getAndReset() {
long stamp = sl.writeLock();
try {
long oldValue = value;
value = 0;
return oldValue;
} finally {
sl.unlockWrite(stamp);
}
}
}线程安全的 LRUCache
public class ThreadSafeLRUCache<K, V> {
private final int capacity;
private final LinkedHashMap<K, V> cache;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public ThreadSafeLRUCache(int capacity) {
this.capacity = capacity;
this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > ThreadSafeLRUCache.this.capacity;
}
};
}
public V get(K key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(K key, V value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
public V remove(K key) {
rwLock.writeLock().lock();
try {
return cache.remove(key);
} finally {
rwLock.writeLock().unlock();
}
}
public int size() {
rwLock.readLock().lock();
try {
return cache.size();
} finally {
rwLock.readLock().unlock();
}
}
public void clear() {
rwLock.writeLock().lock();
try {
cache.clear();
} finally {
rwLock.writeLock().unlock();
}
}
}面试要点
问题1
Q: 读写锁的三个状态是什么? A:
- 读读共享:多个读线程可以同时持有读锁
- 读写互斥:读锁和写锁互斥,获取写锁需要等待所有读锁释放
- 写写互斥:多个写线程互斥,同一时间只有一个写线程
问题2
Q: ReentrantReadWriteLock 的锁降级是什么?如何实现? A: 锁降级是指从写锁降级为读锁。实现步骤:
- 获取写锁
- 获取读锁(在写锁持有期间)
- 释放写锁
- 此时只持有读锁,其他读线程可以并发
- 最后释放读锁
注意:锁升级(从读锁升级为写锁)会导致死锁,不支持。
问题3
Q: StampedLock 与 ReentrantReadWriteLock 有什么区别? A:
| 特性 | ReentrantReadWriteLock | StampedLock |
|---|---|---|
| 乐观读 | 不支持 | 支持 |
| 可重入 | 支持 | 不支持 |
| 性能 | 中等 | 高(乐观读) |
| 使用复杂度 | 简单 | 较复杂 |
| 锁转换 | 不支持 | 支持 |
问题4
Q: 什么是乐观读锁?有什么优势? A: 乐观读锁是一种不加锁的读取方式:
- 工作原理:读取数据后验证是否有写操作,无则使用,有则升级为悲观读
- 优势:读操作完全不加锁,性能最高
- 适用场景:读多写少、读操作频繁且冲突概率低
问题5
Q: 读写锁适用于什么场景? A: 适用于读多写少的场景:
- 缓存系统:读频繁,更新少
- 配置管理:读取频繁,修改少
- 统计信息:频繁查询,偶尔更新
- 数据库查询缓存
不适合:写多读少的场景,反而会降低性能。
问题6
Q: StampedLock 为什么不可重入?有什么风险? A: StampedLock 设计上不可重入,是为了提高性能和简化实现。
风险:
// 错误:会导致死锁
public void method1() {
long stamp = sl.readLock();
try {
method2(); // 再次尝试获取锁会死锁
} finally {
sl.unlockRead(stamp);
}
}
public void method2() {
long stamp = sl.readLock(); // 死锁!
// ...
}解决方案:避免在持有锁时调用其他需要相同锁的方法。
总结
| 锁类型 | 特点 | 适用场景 |
|---|---|---|
| ReentrantLock | 简单互斥 | 写多、竞争激烈 |
| ReentrantReadWriteLock | 读写分离 | 读多写少 |
| StampedLock | 乐观读 | 读极多写极少 |
最佳实践:
- 根据读写比例选择合适的锁
- 写操作用写锁,读操作用读锁
- StampedLock 注意不可重入
- 注意锁的释放顺序
- 高并发场景优先考虑 StampedLock 乐观读
读写锁是高并发场景下的重要优化手段!