Java并发17-读写锁实现缓存
读写锁原则:
- 允许多个线程同时读共享变量
- 同一时间只允许一个线程写共享变量
- 写变量的时候不允许读
快速实现一个缓存
class Cache<K,V> {
final Map<K, V> m =new HashMap<>();
final ReadWriteLock rwl =new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
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, v);
}
finally {
w.unlock();
}
}
}
使用这种缓存,需要先使用put()
将缓存一次性加载好,再提供使用。
按需加载缓存
按需加载,先看缓存中是否有值,有就返回,没有就从数据库中查询。
class Cache<K,V> {
final Map<K, V> m =new HashMap<>();
final ReadWriteLock rwl = new ReentrantReadWriteLock();
final Lock r = rwl.readLock();
final Lock w = rwl.writeLock();
V get(K key) {
V v = null;
//读缓存
r.lock();
try {
v = m.get(key);
} finally{
r.unlock();
}
//缓存中存在,返回
if(v != null) {
return v;
}
//缓存中不存在,查询数据库
w.lock();
try {
//再次验证
//其他线程可能已经查询过数据库
v = m.get(key);
if(v == null){
//查询数据库
v=省略代码无数
m.put(key, v);
}
} finally{
w.unlock();
}
return v;
}
}
关注代码第25行,为什么拿到写锁不直接去数据库取数据,要再验证下v
是否不存在?
假设线程 T1
获取写锁后查询数据库并更新缓存,最终释放写锁。此时线程 T2
和 T3
会再有一个线程能获取写锁。假设是 T2
,如果不再次验证,此时 T2
会再次查询数据库。T2
释放写锁之后,T3
也会再次查询一次数据库。
实际上线程 T1
已经把缓存的值设置好了,T2
、T3
完全没有必要再次查询数据库。
因此再次验证的方式能够避免高并发场景下重复查询数据的问题。
读写锁的升级与降级
- 升级
//读缓存
r.lock(); ①
try {
v = m.get(key); ②
if (v == null) {
w.lock();
try {
//再次验证并更新缓存
//省略详细代码
} finally{
w.unlock();
}
}
} finally{
r.unlock(); ③
}
这种读锁还没释放,在读锁作用范围内使用写锁是不可行的。读锁还未释放,会导致写锁永久等待。
- 降级
class CachedData {
Object data;
volatile boolean cacheValid;
final ReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
void processCachedData() {
// 获取读锁
r.lock();
if (!cacheValid) {
// 释放读锁,因为不允许读锁的升级
r.unlock();
// 获取写锁
w.lock();
try {
// 再次检查状态
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 释放写锁前,降级为读锁
// 降级是可以的
r.lock();
} finally {
// 释放写锁
w.unlock();
}
}
// 此处仍然持有读锁
try {
use(data);
}
finally {
r.unlock();
}
}
}
cacheValid
标记了data
是否可用,即是否有data
缓存。
如果可用,读缓存直接执行到第34行使用数据,再释放锁。
如果不可用,先释放读锁,再获取写锁,再次检查data
是否可用,因为获取到写锁的时候可能已经被其他线程写过data
了,获取data
后将cacheValid
置为true
。
第26行允许释放锁前将写锁降级为读锁,即写锁还未释放时,获取读锁。
特点
读写锁类似于ReentrantLock
,也支持公平模式和非公平模式。
读锁和写锁都实现了 java.util.concurrent.locks.Lock
接口,支持lock()
,tryLock()
、lockInterruptibly()
等方法。
但只有写锁支持条件变量,读锁不支持条件变量,读锁调用 newCondition()
会抛出 UnsupportedOperationException
异常。
思考
线上服务CPU占用率很小,但停止响应了,可能是读写锁升级导致的问题,如何确定?
考虑到是线上应用,可采用以下方法
- 源代码分析。查找
ReentrantReadWriteLock
在项目中的引用,看下写锁是否在读锁释放前尝试获取- 如果线上是Web应用,应用服务器比如说是Tomcat,并且开启了JMX,则可以通过JConsole等工具远程查看下线上死锁的具体情况。
——Kǎfκã²⁰²⁰