首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

NO.37 读写同心锁:探秘ReadWriteLock

加入读书践行群,每天一个知识点,持续精进!

碎片时间体系学习

这是程序员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

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180107G02KOI00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券