ReentrantReadWriteLock不知道大家熟悉吗?其实在实际的项目中用的比较少,反正我所在的项目没有用到过。
ReentrantReadWriteLock称为读写锁,它提供一个读锁,支持多个线程共享同一把锁。它也提供了一把写锁,是独占锁,和其他读锁或者写锁互斥,表明只有一个线程能持有锁资源。通过两把锁的协同工作,能够最大化的提高读写的性能,特别是读多写少的场景,而往往大部分的场景都是读多写少的。
本文主要讲解ReentrantReadWriteLock的使用和应用场景。
ReentrantReadWriteLock实现了ReadWriteLock接口,可以获取到读锁(共享锁),写锁(独占锁)。同时,通过构造方法可以创建锁本身是公平锁还是非公锁。
读写锁机制:
读锁 | 写锁 | |
---|---|---|
读锁 | 共享 | 互斥 |
写锁 | 互斥 | 互斥 |
线程进入读锁的前提条件:
进入写锁的前提条件:
锁升级、降级机制:
我们知道ReentrantLock具备可重入的能力,即同一个线程多次获取锁,不引起阻塞,那么ReentrantReadWriteLock
关于可重入性是怎么样的呢?
关于这个问题需要引入两个概念,锁升级,锁降级。
重入时锁升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写
重入时锁降级支持:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁
API介绍
构造方法:
public ReentrantReadWriteLock()
:默认构造方法,非公平锁public ReentrantReadWriteLock(boolean fair)
:true 为公平锁常用API:
public ReentrantReadWriteLock.ReadLock readLock()
:返回读锁public ReentrantReadWriteLock.WriteLock writeLock()
:返回写锁public void lock()
:加锁public void unlock()
:解锁public boolean tryLock()
:尝试获取锁代码范式
r.lock();
try {
// 临界区
} finally {
r.unlock();
}
复制代码
w.lock();
try {
r.lock();// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
try {
// ...
} finally{
w.unlock();// 要在写锁释放之前获取读锁
}
} finally{
r.unlock();
}
复制代码
@Test
public void readReadMode() throws InterruptedException {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock r = rw.readLock();
ReentrantReadWriteLock.WriteLock w = rw.writeLock();
Thread thread0 = new Thread(() -> {
r.lock();
try {
Thread.sleep(1000);
System.out.println("Thread 1 running " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
},"t1");
Thread thread1 = new Thread(() -> {
r.lock();
try {
Thread.sleep(1000);
System.out.println("Thread 2 running " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
},"t2");
thread0.start();
thread1.start();
thread0.join();
thread1.join();
}
复制代码
运行结果:
@Test
public void readWriteMode() throws InterruptedException {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock r = rw.readLock();
ReentrantReadWriteLock.WriteLock w = rw.writeLock();
Thread thread0 = new Thread(() -> {
r.lock();
try {
Thread.sleep(1000);
System.out.println("Thread 1 running " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
},"t1");
Thread thread1 = new Thread(() -> {
w.lock();
try {
Thread.sleep(1000);
System.out.println("Thread 2 running " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
w.unlock();
}
},"t2");
thread0.start();
thread1.start();
thread0.join();
thread1.join();
}
复制代码
运行结果:
什么场景下读多写少? 想必最先想到的就是缓存把,ReentrantReadWriteLock在缓存场景中就是一个很典型的应用。
缓存更新时,是先清缓存还是先更新数据库?
显而易见,通常情况下,先更新数据库,然后清空缓存。
public class GenericCachedDao {
// 缓存对象,这里用jvm缓存
Map cache = new HashMap<>();
// 读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 读取操作
public String getData(String key) {
// 加读锁,防止其他线程修改缓存
readWriteLock.readLock().lock();
try {
String value = cache.get(key);
// 如果缓存命中,返回
if(value != null) {
return value;
}
} finally {
// 释放读锁
readWriteLock.readLock().unlock();
}
//如果缓存没有命中,从数据库中加载
readWriteLock.writeLock().lock();
try {
// 细节,为防止重复查询数据库, 再次验证
// 因为get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据
String value = cache.get(key);
if(value == null) {
// 这里可以改成从数据库查询
value = "alvin";
cache.put(key, value);
}
return value;
} finally {
readWriteLock.writeLock().unlock();
}
}
// 更新数据
public void updateData(String key, String value) {
// 加写锁
readWriteLock.writeLock().lock();
try {
// 更新操作TODO
// 清空缓存
cache.remove(key);
} finally {
readWriteLock.writeLock().unlock();
}
}
}
复制代码
本文讲解了ReentrantReadWriteLock读写锁常用的API, 以及通过几个demo的演示,讲解了读写锁的使用,希望对大家有帮助。