一、什么是读写锁
一对关联的锁,一个用于只读操作,一个用于写入
读锁可以由多个线程同时持有,写锁是排他的
二、哪些场景适用
适合读取线程比写入线程多的场景,改进互斥锁的性能
比如:缓存组件、集合的并发线程安全性改造
三、读写锁的一些细节注意的地方
- 加了读锁之后,如果未释放,则无法加写锁;必须先释放读锁,才能加写锁
- 而且必须是所有线程都释放了读锁,其他线程才能抢到写锁。
四、JDK ReentrantReadWriteLock 源码中使用读写锁实现缓存的代码理解
源码(在类的注释上)如下:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
解释
首先这个程序在实现一个什么功能?
类似于我们平时使用的缓存,如果缓存数据中存在,则直接从缓存中把数据取出来使用;如果缓存中数据不存在,则从数据库中查询数据,并写入到缓存中。
(假设现在有一千个线程在并发运行)
细节
第 7 行,先加一把读锁,保证在使用的时候,其他线程不能写入。并且可以多线程来读。
第 8 行,判断缓存是否失效(或者数据是否存在)。这里为什么要判断不存在才去查询数据库?不能直接查询数据库吗?
因为大量的线程执行到第 8 行,都发现缓存不存在,都会去查询数据库,就会造成数据库压力增大(缓存雪崩),此时我们会先加一个写锁,只有一个线程能获得到这个锁。
第 11 行,加写锁。写锁只能有一个线程获得。避免所有线程都去查询数据库。拿不到锁的线程(999个线程)在此等待。
第 15 行,再判断一次数据是否存在(双重检查)。如果线程查到了数据,则其他 999 个线程,判断一下发现数据有了就直接返回了。否则 999 个线程还是会去数据库查数据。
第 20 行,加读锁,称为锁降级(写锁未释放的情况下,获得到读锁,随后再释放写锁的过程)。这是为了在数据查询完到返回这个过程,不允许其他线程修改这个值。
第 22 行,释放写锁。但仍然有线程拿着这个读锁,其他线程无法拿到写锁修改这个值。
第 27 行,使用数据。此时仍然被加了读锁,数据无法修改。
第 29 行,释放读锁。