读写锁
开发中经常碰到这样一种场景: 读多写少 ,比如缓存,缓存之所以可以提升性能,一个重要的条件就是缓存的数据一定是读多写少的,缓存中的数据基本不会发生变化(写少),经常性地读缓存的数据(读多)。
针对这种场景,Java SDK并发包提供了读写锁——ReadWriteLock,和比读写锁更快的锁——StampedLock。
1.ReadWriteLock(读写锁)
1.2 什么是读写锁?
读写锁遵循下面这三条规则:
- 允许多个线程同时读共享变量;
- 只允许一个线程进行写共享变量;
- 如果一个线程正在执行写操作,此时禁止其他线程读共享变量。
读写锁和互斥锁的区别是:读写锁允许多个线程进行读操作。
相同点是:读写锁在进行写操作时,是不允许其他线程进行读操作或者写操作。
1.3 使用读写锁快速实现一个缓存?
实现缓存这个工具类,我们提供两个方法,读缓存(get()方法),写缓存(put()方法)。下面是我们的代码:
class Cache<K,V> {
final Map<K, V> m=new HashMap<>();//final域禁止写final域引用对象以及第一次写引用对象成员域到构造函数外,之前文章提到过
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(); }
}
}
下面我们来真正实现缓存:
- 实现缓存的话,首先也需要解决缓存的初始化问题。两种方式:1.一次性加载数据 2.按需加载数据
下面实现缓存的按需加载:在查询缓存中数据,如果不存在就从数据库中查询到缓存中(写缓存需要再次判断是否被其他线程写了),
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;
}
}
- 除此之外,也需要解决缓存数据和源头数据的同步问题,两者需要保证一致性。
解决方法:1.超时机制:当缓存数据超过时效,就让这条数据在缓存中失效,等待再次访问把源数据加载到缓存中。
2.或者在源头数据发生修改,快速反馈给缓存,发生修改了,就将最新的数据存到缓存中。
3.数据库和缓存的双写方案。
1.4 读写锁的升级和降级
1.4.1 ReadWriteLock 是不允许升级的!
先来看一个实例代码:
r.lock(); ①//读缓存
try {
v = m.get(key); ②
if (v == null) {
w.lock();
try {
//省略详细代码 //再次验证并更新缓存
} finally{
w.unlock();
}
}
} finally{
r.unlock(); ③
}
上面中 1.先获取读锁,之后又获取写锁,这叫锁的升级,但是**ReadWriteLock 并不支持这种升级!**上面的代码中,读锁还没有被释放,然后又要获得写锁,会导致写锁永久等待,然后导致线程被阻塞,服务器可能表现cpu利用率低。
1.4.1 ReadWriteLock 允许降级
下面是实现缓存数据按需加载的另外一种方式,直接上代码:
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) { //先判读缓存中是否存在该数据,如果存在跳过if下面的方法,直接use(data)然后释放读锁。
//否则需要写入缓存数据,获取写锁进行写操作,因为不允许锁的升级,所以需要先释放读锁,获取写锁,把数据写入缓存
r.unlock(); // 释放读锁,因为不允许读锁的升级
w.lock(); // 获取写锁
try {
// 获取写锁之前有可能其他线程获取写锁写入了,所以再次检查状态
if (!cacheValid) {//如果没有写入缓存,就写入
data = ...
cacheValid = true;
}
// 因为下面需要释放读锁,我们需要再获得读锁,让其有东西释放,同时允许锁降级,所以直接获得读锁。
r.lock(); // 释放写锁前,降级为读锁,这个锁也需要释放
} finally {
// 释放写锁
w.unlock();
}
}
// 此处仍然持有读锁
try {use(data);}
finally {r.unlock();}//释放读锁
}
}
1.5 读写锁的注意事项
- ReadWriteLock 是一个接口,它的实现类是ReentrantReadWriteLock,通过名字你可以看出来,它是可重入的。
- 同时它获取的读锁和写锁实现了Lock接口,所以除了支持lock()方法外,也支持非阻塞式获取锁tryLock(),lockInterruptibly()等方法。
- 读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。
- 注意:它的写锁支持条件变量,但是读锁是不支持条件变量的,读锁调用newCondition()会抛出异常。