Java多线程编程-(17)-读写锁ReentrantReadWriteLock深入分析

一、前言

上两篇的内容中已经介绍到了锁的实现主要有ReentrantLock和ReentrantReadWriteLock。

ReentrantLock是重入锁,顾名思义就是支持重进入的锁,他表示该锁能够支持一个线程对资源的重复加锁,上文中已经提到在AQS中的同步状态state,如果是0的话表示该资源没有被线程加锁,如果是大于0则表示该资源被当前线程重入的次数。

另外,我们还需要知道最简单的同步关键字synchronized也是支持锁重入的,但他和ReentrantLock相比,synchronized正如他的加锁和释放锁一样都是隐式的,在前几篇文章中也对比了关于Lock和synchronized的区别,这里比在赘述。

另外,ReentrantLock是支持公平锁和非公平锁的,提供了构造函数允许设置是否为公平锁,默认是非公平锁的,这是因为,根据统计衡量如果使用公平锁会有大量的线程上下文切换,而使用非公平锁的话相对较少一些,这也是为什么默认的是使用非公平锁。

上一篇文章在介绍到锁优化的时候,建议将锁分离使用读写锁,这一片我们就一起学习一下读写锁ReentrantReadWriteLock。

二、ReentrantReadWriteLock简介和代码结构

上图中我们可以看到ReentrantReadWriteLock的大致结构。之所以称之为读写锁,是因为ReentrantReadWriteLock内部的两个内部类:ReadLock和WriteLock,这两个内部类实现了最基本的Lock接口。

读写锁维护了一对锁:一个读锁和一个写锁。通过分离读锁和写锁,使得并发性相比一般的排它锁有很大的性能提升。

ReentrantReadWriteLock的特性:

(1)ReentrantReadWriteLock类结构和内部类:

(2)ReentrantReadWriteLock的内部类:

可以看出ReentrantReadWriteLock和ReentrantLock的区别是增加了ReadLock和WriteLock,其他的主要是不同内部类的实现方法的不同,下边看一下Lock相关的接口:

(3)ReadWriteLock接口:

(4)ReadLock和WriteLock接口:

(5)ReadWriteLock提供了readLock()writeLock()方法,类似于工厂方法模式工厂接口,而Lock就是返回的产品接口。而ReentrantReadWriteLock实现了ReadWriteLock接口,那么他就是具体的工厂接口实现类,ReadLock和WriteLock就成了具体产品的实现类,一个简单的工厂方法模式使用案例,值得学习。

根据上述几张图应该大致清楚了各个类和接口之间的关系了。

三、ReentrantReadWriteLock使用案例

通过一个使用HashMap实现的Cache来了解一下ReentrantReadWriteLock的使用,代码如下:

四、ReentrantReadWriteLock原理分析

1、读写锁同步状态的设计

ReentrantLock我们知道他是一个排他锁,使用的是AQS中的一个同步状态state表示当前共享资源是否被其他线程锁占用。如果为0则表示未被占用,其他值表示该锁被重入的次数。

ReentrantReadWriteLock中如何使用一个整数来表示读写状态哪?

由ReentrantReadWriteLock读写锁的特性,我们应该知道需要在AQS的同步状态上维护多个读线程和一个写现成的状态。

如何在一个整型变量上维护多种状态,就需要”按位切割使用” 这个变量,读写锁将变量切分成两个部分,高16位表示读,低16位表示写,划分方式如下图:

当前状态表示一个线程已经获取了写锁,且重入了两次,同时也获取了两次读锁。那么读写锁是如何迅速确定读和写各自的状态那?答案就是”位运算” 。

如何通过位运算计算得出是读还是写获取到锁了那?

如果当前同步状态state不为0,那么先计算低16位写状态,如果低16为为0,也就是写状态为0则表示高16为不为0,也就是读状态不为0,则读获取到锁;如果此时低16为不为0则抹去高16位得出低16位的值,判断是否与state值相同,如果相同则表示写获取到锁。同样如果state不为0,低16为不为0,且低16位值不等于state,也可以通过state的值减去低16位的值计算出高16位的值。上述计算过程都是通过位运算计算出来的。

上图中为什么表示当前状态有一个线程已经获取了写锁,且重入了两次,同时也获取了两次读锁。这是因为:

2、写锁的获取与释放

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者线程不是已经获取写锁的线程,则当前线程进入等待状态。

3、读锁的获取与释放

读锁是一个支持重进入的共享锁。它能够被多个线程同时获取,在没有其他线写线程访问(写状态为0)时,读锁总是会被成功获取,而所作的也只是增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

4、锁降级

看到上述这两段似乎还是找不到为什么会出现高位和低位都不为0的情况怎样确定当前线程获取的是写锁的解答,这就需要我们从另一个需要注意的地方说起:锁降级, 何为锁降级,意思主要是为了保证数据的可见性,假如有一个线程A已经获取了写锁,并且修改了数据,如果当前线程A不获取读锁而直接释放写锁,此时,另一个线程B获取到了写锁并修改了数据,那么当前线程A无法感知线程B的数据更新。如果当前线程A获取读锁,即遵循降级的步骤,则线程B将会被阻塞,直到当前线程A使用数据并释放读锁之后,线程B才能获取写锁进行数据更新。

另外,锁降级中读锁的获取是必要的!!!

正是由于锁降级的存在,才会出现上图中高16位和低16为都不为0,但可以确定是写锁的问题。可以得出结论,如果高16为或者低16为为0,那么我们就可以判断获取到的是写锁或读锁;如果高16位和低16位都不为0那获取到的应该是写锁。就是说如果当前线程已经获取到写锁的话,该线程也是可以通过CAS线程安全的增加读状态的,成功获取读锁。

虽然,为了保证数据的可见性引入锁降级可以将写锁降级为读锁,但是却不可以锁升级,将读锁升级为写锁的,也就是不会出现:当前线程已经获取到读锁了,通过某种方式增加写状态获取到写锁的情况。不允许升级的原因也是保证数据的可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

5、锁降级实例演示:

举个例子更清楚一些,示例是:并发包中ReentrantReadWriteLock读写锁的锁降级模板,代码如下:

五、LockSupport的简要介绍

不管是ReentrantReadWriteLock还是ReentrantLock,当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类完成相应的工作。LockSupport方法如下:

LockSupport以park开头的方法表示阻塞,以unpark开头的方法表示唤醒,具体含义如下:

六、总结

1、读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。

2、如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

3、对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。

4、公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。

5、读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。

参考文章

1、http://www.jianshu.com/p/9f98299a17a5

2、http://www.cnblogs.com/shangxiaofei/p/5807692.html

3、部分内容和截图参考资料《Java并发编程的艺术》

原文发布于微信公众号 - Java后端技术(JavaITWork)

原文发表时间:2017-11-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我是攻城师

深入理解Java8并发工具类StampedLock

StampedLock类是JDK8里面新增的一个并发工具类,这个类比较特殊,在此之前我们先简单的了解一下关于数据库或者存储系统的锁策略和机制。

17520
来自专栏Java职业技术分享

【编程架构实战】——Java并发包基石-AQS详解

 Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共...

6100
来自专栏有趣的Python和你

python爬虫之微打赏(scrapy版)创建项目itemssettingsweidashangspider保存为csv文件

17140
来自专栏Zachary46

Python爬取qq空间说说

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invi...

19230
来自专栏coolblog.xyz技术专栏

AbstractQueuedSynchronizer 原理分析 - 独占/共享模式

AbstractQueuedSynchronizer (抽象队列同步器,以下简称 AQS)出现在 JDK 1.5 中,由大师 Doug Lea 所创作。AQS ...

865140
来自专栏小勇DW3

网页爬虫小记:两种方式的爬取网站内容

此处进行简单的分类,对于普通的网页爬取内容,如果没有登录界面可以直接使用Jsoup的API进行爬取;

21820
来自专栏IMWeb前端团队

Redux 源码解析系列(一) -- Redux的实现思想

Redux 其实是用来帮我们管理状态的一个框架,它暴露给我们四个接口,分别是: createStore combineReducers bindActionCr...

24250
来自专栏java 成神之路

AbstractQueuedSynchronizer 源码分析

36780
来自专栏Python中文社区

多线程爬取斗图网站,赶紧上车

專 欄 ? ❈致Great,Python中文社区专栏作者 博客: http://www.jianshu.com/u/261e23a40f71 ❈ 最近看了Py...

247100
来自专栏云霄雨霁

显式锁

14230

扫码关注云+社区

领取腾讯云代金券