本篇文章主要介绍读写锁ReentrantReadWriteLock相关知识。通过阅读你会有如下收获:
1. 什么是 ReentrantReadWriteLock?
2. 如何通过ReentrantReadWriteLock实现一个本地缓存服务?
3. ReentrantReadWriteLock简单总结
一 . 读写锁简单介绍
读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,实际工作中,为了优化性能,我们经常会使用缓存,例如缓存元数据、缓存基础数据等,这就是一种典型的读多写少应用场景。此外,微服务注册中心也是一个读多写少的典型场景,也是可以使用读写锁来进行优化性能。
所有的读写锁都遵守以下三条基本原则:
- 允许多个线程同时读共享变量;
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。
二. 实战
通过读写锁实现一个懒加载的缓存
完整代码下载:读写锁实现缓存
1. 分析
懒加载,指的是只有当应用查询缓存,并且数据不在缓存里的时候,才触发加载源头相关数据进缓存的操作。
假设缓存的源头是数据库。需要注意的是,如果缓存中没有缓存目标对象,那么就需要从数据库中加载,然后写入缓存,写缓存需要用到写锁,所以在代码调用了 w.lock() 来获取写锁。需要注意的是,在获取写锁之后,我们并没有直接去查询数据库,而是在代码中,重新验证了一次缓存中是否存在,再次验证如果还是不存在,我们才去查询数据库并更新本地缓存(双重检测)。此外,代码中还需要我们注意的是锁降级的使用。
释放写锁前,降级为读锁。
主要是为了保证数据的可见性,如果当前线程不获取读锁,直接释放写锁的话,假设此刻有另一个线程B获取了写锁并修改了数据,那么当前线程无法感知到线程B的数据更新。如果线程获取了读锁,那么线程B将会阻塞,知道当前线程获取了数据并释放读锁之后, 线程B才能获取到写锁并更新数据
2. 代码实现
public class Cache<K, V> {
volatile boolean isCacheVaild;
final Map<K, V> hashMap =
new HashMap<>();
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
final Lock readLock = rwl.readLock();
final Lock writeLock = rwl.writeLock();
/**
* 获取key对应的value值
*
* @param key
* @return
*/
public V get(K key) {
V value = null;
// 读缓存
readLock.lock();
try {
value = hashMap.get(key);
if (value != null) {
return value;
}
} finally {
readLock.unlock();
}
readLock.unlock();
// 缓存中不存在,查询数据库
writeLock.lock();
try {
// 再次验证
// 其他线程可能已经查询过数据库
value = hashMap.get(key);
if (value == null) {
// 查询数据库
value = getDateFromDb();
hashMap.put(key, value);
}
// 释放写锁前,降级为读锁。
// 降级是可以的。
// 主要是为了保证数据的可见性,如果当前线程不获取读锁,直接释放写锁的话,假设此刻有另一个线程B获取了写锁并修改了数据,
// 那么当前线程无法感知到线程B的数据更新。如果线程获取了读锁,那么线程B将会阻塞,知道当前线程获取了数据并释放读锁之后,
// 线程B才能获取到写锁并更新数据
readLock.lock();
} finally {
writeLock.unlock();
}
try {
return value;
} finally {
readLock.unlock();
}
}
/**
* 设置key对应的value值,并返回旧的value
*
* @param key
* @param value
* @return
*/
public V put(K key, V value) {
writeLock.lock();
try {
return hashMap.put(key, value);
} finally {
writeLock.unlock();
}
}
private V getDateFromDb() {
V value = null;
System.out.println("查询数据库");
return value;
}
}
三 .总结
读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了 java.util.concurrent.locks.Lock 接口,所以除了支持 lock() 方法外,tryLock()、lockInterruptibly() 等方法也都是支持的。此外,需要注意的是读写锁支持锁降级(持有写锁,获取读锁,释放写锁的过程),但是不支持锁升级(持有读锁,获取写锁,释放读锁的过程)如此设计也是为了保证数据的可见性,如果多个线程已经获取了读锁,其中任意一个线程成功获取了写锁并更新了数据,那么更新的数据对其他获取读锁的线程是不可见的。
下一篇:【多线程实战 五】-通过COW实现一个RPC框架中的路由表
参考资料
Java并发编程实战 -王宝令
Java并发编程的艺术