浅析Java高并发下的ReadWriteLock读写锁 _ JAVA
对于高频读/低频写的应用场景,使用Lock或者使用synchronized来做同步显然是不太合理的,那么有其他的方式来提高并发性能吗?
在Java的并发包中有许多功能不同的类,今天我们介绍其中的一个,读写锁ReadWriteLock。这种锁在工作中应用场景非常广泛,普遍的使用场景是:对于读多写少的场景。经常用到的例如存储元数据,缓存基础数据等等,这些都是典型的读多写少的应用场景。使用缓存可以极大提升应用程序的处理能力。
读写锁有下面这几个特征:
两个或多个线程可以同时进行读操作
线程正在进行读操作,另一个线程想要进行写操作,另一个线程将会被阻塞直到所有读操作结束
线程正在进行写操作,另一个线程想要进行操作(读或写),另一个线程将会阻塞直到写入方完成操作
读写锁与互斥锁一个重要的区别就是读写锁允许多个线程共享临界段,而互斥锁是不允许的。这是在读多写少场景下读写锁性能优于互斥锁的原因。
但是读写锁在读写操作时是互斥的,当一个线程在进行写操作时,其他的读写线程都是是处于阻塞状态的
读写锁实现缓存
下面我们会来实践,用ReadWriteLock将非线程安全的Map包装为一个简单的缓存工具类
在下面的代码中,我们声明了一个 ICache
ICache这个工具类,我们提供了几种简单常用的方法,其中有读缓存方法 get(),写缓存方法 put()。读缓存需要用到读锁,读锁的使用和前面我们介绍的 Lock 的使用是相同的,都是 try{}finally{}这个编程范式。写缓存则需要用到写锁,写锁的使用和读锁是类似的。这样看来,读写锁的使用还是非常简单的。
class ICache{
private final Mapm = new TreeMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public V get(K k) {
r.lock();
try {
return m.get(k);
} finally {
r.unlock();
}
}
public Object[] allKeys() {
r.lock();
try {
return m.keySet().toArray();
} finally {
r.unlock();
}
}
public V put(K key, V value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
public void clear() {
w.lock();
try {
m.clear();
} finally {
w.unlock();
}
}
}如果你曾经使用过缓存的话,你应该知道使用缓存首先要解决缓存数据的初始化问题。缓存数据的初始化,可以采用一次性加载的方式,也可以使用按需加载的方式。
如果源头数据的数据量不大,就可以采用一次性加载的方式,这种方式最简单,只需在应用启动的时候把源头数据查询出来,依次调用类似上面示例代码中的 put() 方法就可以了。
如果源头数据量非常大,那么就需要按需加载了,按需加载也叫懒加载,指的是只有当应用查询缓存,并且数据不在缓存里的时候,才触发加载源头相关数据进缓存的操作。下面你可以结合文中示意图看看如何利用 ReadWriteLock 来实现缓存的按需加载。
缓存按需加载
文中下面的这段代码实现了按需加载的功能,这里我们假设缓存的源头是数据库。需要注意的是,如果缓存中没有缓存目标对象,那么就需要从数据库中加载,然后写入缓存,写缓存需要用到写锁,所以在代码注释中的5处,我们调用了 w.lock() 来获取写锁。
另外,还需要注意的是,在获取写锁之后,我们并没有直接去查询数据库,而是在代码注释6、7处,重新验证了一次缓存中是否存在,再次验证如果还是不存在,我们才去查询数据库并更新本地缓存。为什么我们要再次验证呢?
class Cache{
final Mapm = new HashMap<>();
final ReadWriteLock rwl = new ReentrantReadWriteLock();
final Lock r = rwl.readLock();
final Lock w = rwl.writeLock();
V cache(K key) {
V v = null;
// 读缓存
r.lock(); //1
try {
v = m.get(key); //2
} finally {
r.unlock(); //3
}
// 缓存中存在,返回
if (v != null) { //4
return v;
}
// 缓存中不存在,查询数据库
w.lock(); //5
try {
// 再次验证
// 其他线程可能已经查询过数据库
v = m.get(key); //6
if (v == null) { //7
// 查询数据库
v = null;//省略代码无数
m.put(key, v);
}
} finally {
w.unlock();
}
return v;
}
}在5处写缓存,需要写锁,在代码6和7处,为什么要重新判断是否存在?
原因是在高并发的场景下,有可能会有多线程竞争写锁。
假设缓存是空的,没有缓存任何东西,如果此时有三个线程 T1、T2 和 T3 同时调用 get() 方法,并且参数 key 也是相同的。那么它们会同时执行到代码注释5处,但此时只有一个线程能够获得写锁,假设是线程 T1,线程 T1 获取写锁之后查询数据库并更新缓存,最终释放写锁。此时线程 T2 和 T3 会再有一个线程能够获取写锁,假设是 T2,如果不采用再次验证的方式,此时 T2 会再次查询数据库。T2 释放写锁之后,T3 也会再次查询一次数据库。而实际上线程 T1 已经把缓存的值设置好了,T2、T3 完全没有必要再次查询数据库。所以,再次验证的方式,能够避免高并发场景下重复查询数据的问题。
读写锁的升级与降级
上面按需加载的示例代码中,在1处获取读锁,在3处释放读锁,那是否可以在2处的下面增加验证缓存并更新缓存的逻辑呢?详细的代码如下。
// 读缓存
r.lock(); //1
try {
v = m.get(key); //2
if (v == null) {
w.lock();
try {
// 再次验证并更新缓存
// 省略详细代码
} finally{
w.unlock();
}
}
} finally{
r.unlock(); //3
}先是获取读锁,然后再升级为写锁,这叫锁的升级。问题:读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。
不过,虽然锁的升级是不允许的,但是锁的降级却是允许的。
以下代码来源自 ReentrantReadWriteLock 的官方示例,略做了改动。你会发现在代码注释1处,获取读锁的时候线程还是持有写锁的,这种锁的降级是支持的。
class CachedData {
private Object data;
private volatile boolean cacheValid; // 缓存无效 true:有效 false 无效
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {// 在获取写锁之前必须释放读锁
rwl.readLock().unlock(); //释放读锁
rwl.writeLock().lock(); //获取写锁
try {
// 重新检查状态,因为另一个线程可能已经获得写锁并在我们之前更改了状态。
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 降级通过获取读锁之前释放写锁
rwl.readLock().lock(); // 1
} finally {
rwl.writeLock().unlock(); // 释放写锁,仍然保持读
}
}
try {
use(data); // 此处仍然持有读锁
} finally {
rwl.readLock().unlock();
}
}
private void use(Object data) {
System.out.println(data.toString());
}
}总结
读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了 java.util.concurrent.locks.Lock 接口,所以除了支持 lock() 方法外,tryLock()、lockInterruptibly() 等方法也都是支持的。
但是有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。另外,官方文档中还提到了,读写锁支持最多65535个递归写锁和65535个读锁。如果超过这个限制会导致锁定方法抛出错误。
推荐阅读
-
4个理由告诉你Java为何排行第一
本文由码农网 –单劼原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!Java已经有20年的历史了,甚...
-
写给精明Java开发者的测试技巧
我们都会为我们的代码编写测试,不是吗?毫无疑问,我知道这个问题的答案可能会从“当然,但你知道怎样才能避免写测试吗?”到“必须...
-
Java 微服务框架 Redkale 入门介绍
Redkale功能Redkale虽然只有1.xM大小,但是麻雀虽小五脏俱全。既可作为服务器使用,也可当工具包使用。作为独立的工...
-
Java内存管理原理及内存区域详解
一、概述Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁...
-
2015年Java开发岗位面试题归类
下面是我自己收集整理的Java岗位今天面经遇到的面试题,可以用它来好好准备面试。一、Java基础1.String...
-
Java 虚拟机类加载机制和字节码执行引擎
引言我们知道java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢?...
-
Java常量池理解与总结
一.相关概念什么是常量用final修饰的成员变量表示常量,值一旦给定就无法改变!final修饰的变量有三种:静态...
-
Java 实现线程死锁
概述春节的时候去面试了一家公司,笔试题里面有一道是使用简单的代码实现线程的‘死锁’,当时没有想到这道题考的是Sync...
-
Java:过去、未来的互联网编程之王
Java对你而言是什么?一门你大学里学过的语言?一个IT行业的通用语言?你相信Java已经为下一次互联网爆炸做好了准备么?Java...
-
20个高级Java面试题汇总
本文由码农网 –小峰原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!这是一个高级Java面试系列题中...
