第二部分:并发工具类17->ReadWriteLock:如何快速实现一个完备的缓存
1.其他工具类
用途:分场景优化性能,提升易用性
2.并发场景,读多写少
使用缓存,缓存元数据,缓存基础数据
缓存的数据一定是读多写少
3.读写锁ReadWriteLock
非常容易使用,性能很好
1.允许多个线程同时读共享变量
2.只允许一个线程写共享变量
3.如果一个写线程正在执行写操作,此时禁止读线程读共享变量
读写锁与互斥锁ReentrantLock重要区别就是读写锁允许多个线程同时读共享变量,互斥锁不允许
读写锁在读多写少场景下性能优于互斥锁的关键
读写锁的写操作是互斥的,当一个线程在写共享变量时,不允许其他线程执行写操作和读操作
4.使用读写锁ReadWriteLock,用在缓存上
Cache<K,V> 类,参数K代表缓存里key的类型,V代表缓存value的类型,都是泛型
缓存数据保存在Cache类的内部hashmap中,hashmap不是线程安全,使用读写锁ReadWriteLock保证线程安全
ReadWriteLock是接口,实现类ReentrantReadWriteLock,
缓存类的方法get和pu都用到了读写锁
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(); }
}
}
5.缓存的扩展知识
1.解决缓存数据初始化问题,可以采用一次性加载,也可以使用按需加载
2.源头数据不大,可以一次性加载方式,这种方式最简单,应用启动时把源头数据查出来,依次调用put方法就可以
3.源头数据大,按需加载,也成为懒加载,当应用查询缓存,数据不在缓存里,才触发加载源头相关数据进缓存
6.缓存按需加载
数据源头是数据库,如果缓存中没有缓存目标对象,就需要从数据库中加载,然后写入缓存,写缓存需要用到写锁,w.lock()
在获取写锁后,没有直接查库,而是重新验证了一次缓存中是否存在,不存在才会去查库?
高并发下,多线程竞争写锁,缓存是空的,线程T1获取写锁后,直接查询并更新缓存,释放锁,线程T2会再次获取到锁,如果不验证,会再次查库
所以多加一部验证,能避免高并发场景下重复查询数据的问题
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;
}
}
7.锁的升级
先获取读锁,然后再获取写锁;这种是不允许的,ReadWriteLock
8.总结
读写锁类似ReentrantLock,也支持公平锁和非公平锁
只有写锁支持条件变量,读锁不支持条件变量
newCondition()
双写方案,写缓存+写数据库
超时机制,缓存不是长久有效,缓存的数据超过时效,缓存中就实效了。