使用管程和信号量可以处理一切并发问题,但在一些场景下,可能并非最优解
在日常开发中,读多写少是很常见场景,比如一个功能完备的缓存系统
当数据不经常发生变化,仅仅是读取次数多,那么就没必要频繁加锁损耗性能
读写锁是适用于对读多写少场景的锁,是一种通用的技术
读写锁一般满足:读读不互斥、读写互斥、写写互斥
读写锁和普通互斥锁的区别就是读写锁允许多线程同时读取共享变量
Java 中并发包提供了读写锁,如 ReadWriteLock 和 StampedLock
ReadWriteLock 读写锁,一般使用其实现类 ReentrantReadWriteLock
使用 ReentrantReadWriteLock 创建读写锁
构造函数可接受一个布尔值指定是否公平锁
private final ReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
private final Lock r = rwl.readLock();
// 写锁
private final Lock w = rwl.writeLock();
当线程持有写锁时,其他线程的操作需要等待
当线程持有读锁,其他线程也可以获取读锁,但获取写锁需要等待
需要注意:只有写锁支持创建 Condition
ReadWriteLock 实现简易缓存
class Cache<K, V> {
private final Map<K, V> m = new HashMap<>();
private final ReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
// 读缓存
V get(K key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
// 写缓存
V put(K key, V value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
}
ReadWriteLock 锁的升级和降级
在一个读锁释放之前申请写锁叫锁升级,ReentrantReadWriteLock 不支持,会导致永久等待
在一个写锁释放之前申请读锁叫锁降级,ReentrantReadWriteLock 支持
w.lock();
// 当前线程写数据...
// 获取读锁
r.lock();
// 释放写锁
w.unlock();
// 所有线程读数据...
r.unlock();
降级可以保证修改数据后立即获取,避免其他线程修改导致获取不到最新数据,保证了可见性
ReadWriteLock 支持锁和读锁,读的过程中不能写,是一种悲观的读锁
StampedLock 也叫戳记锁,在 JDK8 中引入,支持写锁、悲观读锁、乐观读锁
StampedLock 是不可重入的,写锁和悲观读需要解锁后
使用写锁和悲观读锁
返回一个 long 型值,类似于版本戳
StampedLock stampedLock = new StampedLock();
// 获取/释放悲观读锁
long stamp = stampedLock.readLock();
try {
// ...
} finally {
stampedLock.unlockRead(stamp);
}
// 获取/释放写锁
long stamp = stampedLock.writeLock();
try {
// ...
} finally {
stampedLock.unlockWrite(stamp);
}
使用乐观读锁
乐观读锁不需要释放,仅仅相当于一个用于观察数据是否改变的戳记
// 获取一个乐观读锁
long stamp = stampedLock.tryOptimisticRead();
// ...
// 检查获取乐观读锁后是否有写操作发生
if (!stampedLock.validate(stamp)) {
// 申请悲观读锁
stamp = stampedLock.readLock();
try {
// ...
} finally {
// 释放悲观读锁
stampedLock.unlockRead(stamp);
}
}
乐观读锁的思路和利用版本戳更新数据库的思路很相似,都是先不加锁,更新数据时进行冲突检查
在读多写少的情况下,可以避免频繁加锁,比 ReadWriteLock 效率更高
tryOptimisticRead 没有设置锁状态,不需要显式的释放锁
StampedLock 锁的互相转换
tryConvertToWriteLock(long stamp) 将 stamp 转换为写锁
StampedLock stampedLock = new StampedLock();
// 获取读锁
long stamp = stampedLock.readLock();
// 转换为写锁
long wStamp = stampedLock.tryConvertToWriteLock(stamp);
// 转换失败则返回 0L
if (wStamp != 0L) {
stamp = wStamp;
} else {
// 释放读锁再申请写锁
}
// 释放锁
stampedLock.unlock(stamp);
tryConvertToReadLock(long stamp) 将 stamp 转换为悲观读锁
tryConvertToOptimisticRead(long stamp) 将 stamp 转换为乐观读锁
StampedLock 锁的转换方法是非阻塞的,返回值情况如下:
若当前已经持有目标锁,则立即返回
若当前目标锁可用时,释放当前锁,返回目标锁
其他情况返回 0L 表示转换失败