加入读书践行群,每天一个知识点,持续精进!
碎片时间体系学习
这是程序员chatbook第91篇原创
今天是
2018年的第7天
今日难度系数 :
预计阅读时间 :5分钟
00、ReadWriteLock接口
本文主要讲讲ReadWriteLock接口及其实现类ReentrantReadWriteLock;该篇文章可看作是介绍Lock接口(NO.35 寂寞沙洲冷:Lock与ReentrantLock)的那篇文章的姊妹篇。
1
publicinterfaceReadWriteLock {
/**
* Returns the lock used for reading.
*
*@returnthe lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
*@returnthe lock used for writing
*/
Lock writeLock();
}
从源码及其注释中,我们可以得到:
ReadWriteLock并不是从字面上所表达那样,不是Lock的子接口,而是一种新的锁接口,只是用到了Lock接口,实现其特定功能
ReadWriteLock提供了两个锁——读取锁、写入锁;其中每次读取共享数据时需要使用读取锁,当需要修改共享数据时就需要使用写入锁
01、ReentrantReadWriteLock实现类
ReentrantReadWriteLock是ReadWriteLock接口的实现类。从ReentrantReadWriteLock字面上来看包括了两重含义:一层含义是读写锁;另外一层含义是具有可重入性。
读写锁就是每次读取共享数据时使用读取锁,当需要修改共享数据时就使用写入锁。
可重入性就是如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读锁;如果一个线程对资源加了读锁,其他线程可以继续加读锁。
综上,以下是对ReentrantReadWriteLock的读写锁机制的总结:
读操作-读操作不互斥:没有发生写操作,当多个线程同时执行读操作,那么这多个线程可以并发执行,不会发生阻塞
写操作-读操作互斥:当前正在发生写操作,那么此刻到来的读线程就会被阻塞
读操作-写操作互斥:当前正在发生读操作,那么此刻到来的写线程就会被阻塞
写操作-写操作互斥:当多个线程同时执行写操作时,某个线程先拿到锁就先执行,其他线程会被阻塞直到之前线程释放锁
02、ReentrantReadWriteLock的应用
关于ReentrantReadWriteLock的典型用法一般有两种。
读操作与写操作相分离的场景,具体实现见代码2。
2
//读写锁的典型用法
publicclassReadAndWrite {
//定义一个读写锁
privateReentrantReadWriteLockrwLock=newReentrantReadWriteLock();
//获取一个可以被多个读操作可共享的读取锁,同时互斥所有写操作
privateLockreadLock=rwLock.readLock();
//获取一个只能独占的写入锁,同时互斥所有的读操作与写操作
privateLockwriteLock=rwLock.writeLock();
privateintcost;
//对所有读操作加读锁
publicintget(){
//关于锁的用法与Lock的用法类似
//需要配合try-finally使用
readLock.lock();
try{
//TODO
}finally{
readLock.unlock();
}
returncost;
}
//对所有写操作加写锁
publicvoidset() {
//关于锁的用法与Lock的用法类似
//需要配合try-finally使用
writeLock.lock();
try{
//TODO
}finally{
// 释放锁
writeLock.unlock();
}
}
}
读操作与写操作相混合的场景,具体实现见代码3,该代码同时是JDK官方的例子,下面将掰开揉碎进行注释说明其用法与原理。
3
classCachedData {
//缓存的数据内容
Objectdata;
//标识缓存数据是否准备好
volatilebooleancacheValid;
//读写锁
finalReentrantReadWriteLockrwl=newReentrantReadWriteLock();
voidprocessCachedData() {
//为了先从缓存中获取数据,首先申请读锁
rwl.readLock().lock();
//如果缓冲中的数据还未准备好
if(!cacheValid) {
//由于缓冲中的数据还未准备好,我们首先需要从数据库中获取数据源
//但是为了执行写操作,需要申请写锁,由于读锁与写锁互斥
//需要首先是否读锁
rwl.readLock().unlock();//语句A
//为了写入数据,需要首先申请写锁
rwl.writeLock().lock();
try{
//需要再次判断,因为这里存在一种情况:比如之前有两个(甲与乙)读线程都执行到了语句A
//其中甲线程获取到了写锁,开始执行;而乙线程被阻塞
//等待甲执行完后,释放了写锁;乙线程获取到了写锁会继续执行
//但是,此时甲线程已执行了语句B,已经将cacheValid置为了true,
//下述的if判断可保证乙线程不再重复从数据库中获取数据源
if(!cacheValid) {
data = ...
cacheValid =true;//语句B
}
//写操作完成时,必须进行锁降级,为什么是必须?请看下文
//即:释放写锁之前先获取读锁
rwl.readLock().lock();//语句C
}finally{
//释放写锁,但是仍然持有读锁
rwl.writeLock().unlock();
}
}
//缓冲的中的数据已准备好
try{
//用户的业务逻辑,使用数据
use(data);
}finally{
//使用完数据后,最后释放读锁
rwl.readLock().unlock();
}
}
}
在代码3的注释中,提出了一个问题,即在语句C中,写操作完成时,为什么必须进行锁降级?我能不能直接就释放了写锁,而不需要事先获取读锁?
答案是否定的!
这主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另外一个线程获取了写锁并修改了数据,那么当前线程无法感知后者线程的数据更新。如果当前线程获取了读锁,即遵循了锁降级的步骤,由于读-写互斥,后续线程就会被阻塞,直到当前线程使用数据并释放读锁后,后续线程线程才能获取写锁进行数据更新。
锁降级是掌握读写锁的重点与难点,欢迎老铁们在留言区写下您的感悟与大家共同讨论。
如果觉得文章有用,感谢老铁们转发分享,让更多的小伙伴建立连接!
【参考资料】
1 Java多线程编程实战指南, 黄文海, 中国工信出版集团。
2 Java并发编程从入门到精通,张振华,清华大学出版社。
程序员Chatbook
领取专属 10元无门槛券
私享最新 技术干货