建议:学习本文前,建议先了解一下 可重入锁 ReentrantLock ,理解一下"可重入"是个什么概念 ^_^
概念
既然已经有了 可重入锁 ReentrantLock,为什么还要有 可重入读写锁 ReentrantReadWriteLock 呢??
因为 可重入锁 ReentrantLock 和 synchronized 一样,属于独占锁。所谓"独占",即:同一时间只能有一个线程持有锁。
ReentrantReadWriteLock 的出现, 目的就是解决 ReentrantLock 独占锁 带来的性能问题。使用 ReentrantLock 无论是 "写/写"线程、"读/读"线程、"读/写"线程之间的工作都是互斥,同时只能有一个线程进入同步区域。
然而大多数场景下,"读/读"线程之间并不会存在互斥的关系,只有"读/写"线程 或 "写/写"线程间的操作才需要互斥。因此,可重入读写锁 ReentrantReadWriteLock 就出现了。
特性
ReentrantReadWriteLock 实现的是 ReadWriteLock 接口。而并不是 Lock接口(ReentrantLock实现的是Lock接口)
该接口提供了 readLock 和 writeLock 两种锁的操作机制,一个是读锁,一个是写锁。
读锁是 共享锁,写锁是 独占锁(排他锁)
实例:一个资源可以被多个读操作访问,或者一个资源一个写操作访问;但是一个资源不能同时 读操作 && 写操作 并行,这样有可能会导致脏数据的问题。
ReentrantReadWriteLock 的出现,就是为了提高读操作的吞吐量。
可重入读写锁Demo
/**
* TODO 可重入读写锁 ReentrantReadWriteLock
*
* @author liuzebiao
* @Date 2019-12-25 18:33
*/
public class RWLockDemo {
static Map<String, Object> cacheMap = new HashMap<>();
static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock read = readWriteLock.readLock(); //读锁
static Lock write = readWriteLock.writeLock();//写锁
//场景:缓存的更新和读取时,
//遇到问题:当前某个线程更新了数据。其他线程在更新前读到了这个数据,导致脏数据问题。
//解决方法:可以通过读写锁的方式,来解决这个问题
public static void get(String key) {
//读锁(执行读操作时,需要获取一个读锁,并发访问时,并不会被阻塞。当我们有10个、100个线程访问时,加了读锁,并不会影响这100个线程的读操作)
//因为读并不会改变数据的状态。可以认为没加锁一样,在此处只是一个锁的控制,读锁并不会被阻塞
read.lock();
try {
System.out.println("读到:[key=" + key + ",value=" + cacheMap.get(key) + "]");
} finally {
read.unlock();
}
}
public static void set(String key, Object value) {
//写锁(执行写操作时,线程必须获得一个写锁,此时,当已经有其他线程获得了写锁的话,当前线程在写的时候,会因获取不到这个锁而被阻塞)
//只有当这个锁被释放以后,其他的写操作才能继续
write.lock();
try {
cacheMap.put(key,value);
System.out.println("写入:[key=" + key + ",value="+value + "]");
} finally {
write.unlock();
}
}
//某线程获得写锁后,其他线程去读 get()读操作时,会被阻塞在 read.lock() 处。即当前获得写锁,以为这当前数据有可能发生变化。
//其他线程在get()获得数据时,有可能造成脏数据。所以在 read.lock() 处会阻塞。当写锁被释放后, read.lock() 处的读操作就能继续进行
public static void main(String[] args) {
//写线程(开启3个线程)
//因为读线程并不会被阻塞,所以 i 定义从25开始,读写线程有交集,从而能够保证读线程能够读取到写入的值
for (int i = 25; i < 28; i++) {
String j = String.valueOf(i);
new Thread(() -> {
RWLockDemo.set("read" + j, Math.ceil(Math.random()*1000));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
//读线程(开启30个线程)
for (int i = 0; i < 30; i++) {
String j = String.valueOf(i);
new Thread(() -> {
RWLockDemo.get("read"+j);
}).start();
}
}
}
测试结果
你拿 demo 自己测试一下,你就会发现读操作根本不会有任何阻塞,是瞬间进行的。而写操作就不一样了。
本实例基本 3秒 完事,如果你将本例 ReentrantReadWriteLock 换做 ReentrantLock(独占锁),那时间就得基本 33秒 才能完成了。
读到:[key=read0,value=null]
读到:[key=read1,value=null]
读到:[key=read3,value=null]
写入:[key=read25,value=651.0]
写入:[key=read27,value=552.0]
写入:[key=read26,value=870.0]
读到:[key=read2,value=null]
读到:[key=read6,value=null]
读到:[key=read11,value=null]
读到:[key=read4,value=null]
读到:[key=read8,value=null]
读到:[key=read13,value=null]
读到:[key=read15,value=null]
读到:[key=read7,value=null]
读到:[key=read9,value=null]
读到:[key=read12,value=null]
读到:[key=read24,value=null]
读到:[key=read25,value=651.0]
读到:[key=read26,value=870.0]
读到:[key=read28,value=null]
读到:[key=read23,value=null]
读到:[key=read27,value=552.0]
读到:[key=read21,value=null]
读到:[key=read18,value=null]
读到:[key=read17,value=null]
读到:[key=read16,value=null]
读到:[key=read14,value=null]
读到:[key=read5,value=null]
读到:[key=read22,value=null]
读到:[key=read20,value=null]
读到:[key=read19,value=null]
读到:[key=read10,value=null]
读到:[key=read29,value=null]
原理分析
请参考大神文章:https://www.jianshu.com/p/9f98299a17a5
可重入读写锁 ReentrantReadWriteLock 的用法,介绍到此为止
如果本文对你有所帮助,那就给我点个赞呗 ^_^
End