关于读写锁

Java语法层面的synchronized锁和JDK内置可重入锁ReentrantLock我们都经常会使用,这两种锁都属于纯粹的独占锁,也就是说这些锁任意时刻只能由一个线程持有,其它线程都得排队依次获取锁。

为了提高并发性能我们会额外引入共享锁来与独占锁共同对外构成一个锁,这种就叫读写锁。

为什么叫读写锁呢?主要是因为它的使用考虑了读写场景,一般认为读操作不会改变数据所以可以多线程进行读操作,但写操作会改变数据所以只能一个线程进行写操作。

读写锁在内部维护了一对锁(读锁和写锁),它通过将锁进行分离从而得到更高的并发性能。

如下图中,存在一个读写锁,其内部包含了读锁和写锁两个对象。假如存在五个线程,其中线程一和线程二想要获取读锁,那么两个线程是可以同时获取到读锁的。但是写锁就不可以共享,它是独占锁。

比如线程三、线程四和线程五都想要持有写锁,那么只能一个个线程轮着持有。

推荐观看

B站20W播放:

Java架构师必会六大核心知识点:多线程、JVM、设计模式、MySQL、Redis、ZooKeeper​www.bilibili.com

B站18W播放:

2020最新马士兵老师讲解多线程与高并发—吊锤P8面试官​www.bilibili.com

读写锁的性质

  • 可以多个线程同时持有读锁,某个线程成功获取读锁后其它线程仍然能成功获取读锁,即使该线程不释放读锁。
  • 在某个线程持有读锁的情况下其它线程不能持有写锁,除非持有读锁的线程全部都释放掉读锁。
  • 在某个线程持有写锁的情况下其它线程不能持有写锁或读锁,某个线程成功获取写锁后其它所有尝试获取读锁和写锁的线程都将进入等待状态,只有当该线程释放写锁后才其它线程能够继续往下执行。
  • 如果我们要获取读锁则需要满足两个条件:目前没有线程持有写锁和目前没有线程请求获取写锁。
  • 如果我们要获取写锁则需要满足两个条件:目前没有线程持有写锁和目前没有线程持有读锁。

简单版本的读写锁

为了加深对读写锁的理解,在分析JDK实现的读写锁之前我们先来看一个简单的读写锁实现版本。其中三个整型变量分别表示持有读锁的线程数、持有写锁的线程数以及请求获取写锁的线程数,四个方法分别对应读锁、写锁的获取和释放操作。

acquireReadLock方法用于获取读锁,如果持有写锁的线程数量或请求读锁的线程数大于0则让线程进入等待状态。

releaseReadLock方法用于释放读锁,将读锁线程数减一并唤醒其它线程。

acquireWriteLock方法用于获取写锁,如果持有读锁的线程数量或持有写锁的线程数量大于0则让线程进入等待状态。

releaseWriteLock方法用于释放写锁,将写锁线程数减一并唤醒其它线程。

读锁升级为写锁

在某些场景下,我们希望某个已经拥有读锁的线程能够获得写锁,并将原来的读锁释放掉,这种情况就涉及到读锁升级为写锁操作。读写锁的升级操作需要满足一定的条件,这个条件就是某个线程必须是唯一拥有读锁的线程,否则将无法成功升级。

如下图中,线程二已经持有读锁了,而且它是唯一的一个持有读锁的线程,所以它可以成功获得写锁。

写锁降级为读锁

与锁升级相对应的是锁降级,锁降级就是某个已经拥有写锁的线程希望能够获得读锁,并将原来的写锁释放掉。锁降级操作几乎没有什么风险,因为写锁是独占锁,持有写锁的线程肯定是唯一的,而且读锁也肯定不存在持有线程,所以写锁可以直接降级为读锁。

如下图中,线程三持有写锁,此时其它线程不可能持有读锁和写锁,所以可以安全地将写锁降为读锁。

ReentrantReadWriteLock类图

为了更清晰理解ReentrantReadWriteLock类的结构,我们先来看它的类图。该类包含的属性和方法较多,为了让分析思路清晰且方便读者理解,我们将剔除非核心源码,只对核心功能进行分析。

ReentrantReadWriteLock类实现了Serializable接口和ReadWriteLock接口,前者用于序列化,而后者则提供了readLock()和writeLock()两个方法。该类的内部同步器Sync基于AQS同步器实现,即继承了AbstractQueuedSynchronizer类。

同步器分为公平模式和非公平模式,分别对应着FairSync类和NonfairSync类。其中公平/非公平模式表示多个线程同时去获取锁时是否按照先到先得的顺序获得锁,如果是则为公平模式,否则为非公平模式。

该类还包含了读锁和写锁,分别对应ReadLock类和WriteLock类,它们都属于ReentrantReadWriteLock的内部类,且都集成了Lock接口。

ReentrantReadWriteLock实现思想

总的来说,ReentrantReadWriteLock类中不管是公平模式还是非公平模式、不管是读锁还是写锁都是基于AQS同步器来实现的。实现的主要难点在于只使用一个AQS同步器对象来实现读锁和写锁,这就要求读锁和写锁共用同一个共享状态变量。

我们知道AQS同步器的共享状态是整型的,即32位,那么最简单的共用方式就是读锁和写锁分别使用16位。其中高16位用于读锁的状态,而低16位则用于写锁的状态,这样便达到共用效果。

但是这样设计后当我们要获取读锁和写锁的状态值时则需要一些额外的计算,比如一些移位和逻辑与操作。

ReentrantReadWriteLock的同步器共用状态变量的逻辑如下,其中SHARED_SHIFT表示移动的位数为16;SHARED_UNIT表示读锁每次加锁对应的状态值大小,1左移16位刚好对应高16位的1;MAX_COUNT表示读锁能被加锁的最大次数,值为16个1(二进制);EXCLUSIVE_MASK表示写锁的掩码,值为16个1(二进制)。

sharedCount方法用于获取读锁(高16位)的状态值,左移16位即能得到。exclusiveCount方法用于获取写锁(低16位)的状态值,通过掩码即能得到。

ReadLock与WriteLock

ReadLock与WriteLock是ReentrantReadWriteLock的两个要素,它们都实现了Lock接口,我们主要关注lock、unlock和newCondition这三个核心方法。

分别表示对读锁和写锁的加锁操作、释放锁操作和创建Condition对象操作,可以看到这些方法都间接调用了同步器Sync的方法,需要注意的是读锁不支持创建Condition对象。

公平/非公平模式

ReentrantReadWriteLock的默认模式为非公平模式,其中同步器Sync是公平模式FairSync类和非公平模式NonfairSync类的抽象父类。

因为ReentrantReadWriteLock的读锁使用了共享模式,而写锁使用了独占模式,所以该父类将不同模式下的公平机制抽象为readerShouldBlock和writerShouldBlock两个抽象方法,然后子类就可以各自实现不同的公平模式。

换句话说,ReentrantReadWriteLock的公平机制就由这两个方法来决定了。

下面就是公平模式和非公平模式的差异,公平模式的readerShouldBlock和writerShouldBlock两个方法都直接返回hasQueuedPredecessors方法的结果,表示如果等待队列中有其它线程则让当前线程加入等待队列中,从而保证公平性。

而非公平模式的writerShouldBlock方法直接返回false,表示不让当前线程进入等待队列,而是直接进行锁的获取竞争。readerShouldBlock方法则调用apparentlyFirstQueuedIsExclusive方法判断头结点的下一个节点线程是否在请求获取独占锁(写锁)。

如果是则让其它线程先获取写锁,而自己则乖乖去排队。如果不是则说明下一个节点线程是请求共享锁(读锁),此时直接与之竞争读锁。

写锁的实现

WriteLock有两个核心方法:lock和unlock。它们都会间接调用内部同步器sync对应方法,在同步器中需要重写tryAcquire方法和tryRelease方法,分别用于获取写锁和释放写锁操作。

先看tryAcquire方法的逻辑,c!=0时有两种情况,一种是高16位的读锁状态不为0,一种是低16位的写锁状态不为0。w等于0时表示还有线程持有读锁,直接返回false表示获取写锁失败。

如果持有写锁的线程为当前线程,则表示写锁重入操作,此时需要将状态变量进行累加,此外需要校验的是写锁重入状态值不能超过MAX_COUNT。

通过writerShouldBlock方法判断是否需要将当前线程放入排队队列中,同时通过CAS方式对状态变量进行累加操作。

对于非公平模式,这里就是闯入操作,即线程先尝试一次竞争写锁。最后设置当前线程持有写锁。

继续看tryRelease方法的逻辑,先用isHeldExclusively方法检查当前线程必须为写锁持有线程,然后将状态值减去释放的值并获取写锁状态值,如果其值为0则表示不存在重入情况,可以彻底释放锁了。

设置无线程持有写锁,最后设置新的状态值。

读锁的实现

读锁需重写同步器中的tryAcquireShared方法和tryReleaseShared方法,它们分别用于获取读锁和释放读锁操作。

tryAcquireShared方法的逻辑为:先获取写锁状态,如果不为0则表示有其它线程持有写锁而且当前线程没有持有写锁,则此时尝试获取读锁失败,将当前线程放到排队队列。

注意这里如果当前线程持有写锁的话则可以继续获取读锁。然后获取读锁状态,尝试通过CAS设置新的状态值,如果成功则返回1表示成功获取读锁。如果不成功则继续调用fullTryAcquireShared方法,该方法主要是一个自旋操作。

如果写锁不为0且当前线程未持有写锁则返回-1,表示尝试获取读锁失败,将当前线程加入排队队列中。如果写锁的状态为0,则表示没有线程持有写锁,继续通过readerShouldBlock方法判断是否需要将该线程加入到排队队列中。

此外,读锁的状态值不能等于MAX_COUNT,最后通过CAS方式设置新的状态值。

tryReleaseShared方法的逻辑为:通过for循环实现自旋,自旋的逻辑就是不断计算新的状态值,然后通过CAS方式来设置新的状态值。

一个例子

接着我们看一个读写锁的例子,看看如何使用读写锁。首先创建ReentrantReadWriteLock对象,然后通过它的读锁和写锁来访问线程不安全的TreeMap对象。

其中get方法属于读取数据的操作,所以使用共享的读锁即可。

而put和clear两个方法涉及到修改数据的操作,需要使用独占的写锁

推荐观看

B站20W播放:

Java架构师必会六大核心知识点:多线程、JVM、设计模式、MySQL、Redis、ZooKeeper​www.bilibili.com

B站18W播放:

2020最新马士兵老师讲解多线程与高并发—吊锤P8面试官​www.bilibili.com

总结

本文介绍了Java中的读写锁ReentrantReadWriteLock的相关知识,并深入讲解了它的实现原理。在ReentrantReadWriteLock读写锁中,写锁是一种独占锁,包括了公平模式和非公平模式。

而读写则是一种共享锁,它也包含了公平模式和非公平模式。它的实现基于AQS同步器,其中最重要的点是它通过某些技巧让读锁和写锁公共了同一个状态变量。通过本文的讲解相信大家已经很好地掌握了JDK提供的读写锁的实现原理。

对变量移位顺序读写_Java多线程并发读写锁ReadWriteLock实现原理剖析相关推荐

  1. Java并发原理抽丝剥茧,读写锁ReadWriteLock实现深入剖析

    跟着作者的65节课彻底搞懂Java并发原理专栏,一步步彻底搞懂Java并发原理. 作者简介:笔名seaboat,擅长工程算法.人工智能算法.自然语言处理.架构.分布式.高并发.大数据和搜索引擎等方面的 ...

  2. java 读写锁_Java中的读写锁

    一.读写锁 1.初识读写锁 a)Java中的锁--Lock和synchronized中介绍的ReentrantLock和synchronized基本上都是排它锁,意味着这些锁在同一时刻只允许一个线程进 ...

  3. Java多线程学习十六:读写锁 ReadWriteLock 获取锁有哪些规则

    读写锁 ReadWriteLock 获取锁有哪些规则呢? 在没有读写锁之前,我们假设使用普通的 ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源,因为如果多个读操作同时进 ...

  4. java线程钥匙_Java多线程并发编程/锁的理解

    一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等 ...

  5. 线程执行完之后会释放吗_java多线程并发:CAS+AQS+HashMap+volatile+ThreadLocal,乐分享...

    CyclicBarrier.CountDownLatch.Semaphore 的用法 CountDownLatch(线程计数器 ) CountDownLatch 类位于 java.util.concu ...

  6. java 计时器_Java多线程并发容器之并发倒计时器

    从火箭发场景来学习Java多线程并发闭锁对象 倒计时器场景 在我们开发过程中,有时候会使用到倒计时计数器.最简单的是:int size = 5; 执行后,size-这种方式来实现.但是在多线程并发的情 ...

  7. java中thread实例_Java多线程并发执行demo代码实例

    主类:MultiThread,执行并发类 package java8test; import java.util.ArrayList; import java.util.List; import ja ...

  8. java 多线程 变慢_java多线程并发程序执行慢有什么原因?该怎么解决?

    我们在执行java多线程并发程序时有时候会碰到执行特别慢的场景,小伙伴们知道是什么原因导致的吗?它要怎么解决呢?下面小编就为你讲讲. 前提:在某地需要开发一个应用系统,此系统主要功能是能够让一些中小型 ...

  9. c++ 并发-读写锁(shared_mutex,shared_lock)

    目录 1.前言 2.正文 3.代码和验证 1.前言: 前几日上班时,因为底层逻辑修改.多线程情况下,许多函数如果依然按照原有的锁去串行执行,效率会很低.(原先是 std::mutex ,以及每个函数用 ...

最新文章

  1. 遭遇一次MySQL猜解注入攻击
  2. erlang mysql连接超时_Erlang数据库-(一)Erlang与Mysql的连接
  3. centos8 挂载ntfs_CentOS 8 挂载NTFS系统磁盘方案
  4. 去广州见了我大学老师标哥
  5. dataGrid列比较多内容显示不全的扩展
  6. HTML5 Canvas中绘制贝塞尔曲线
  7. 【jQuery笔记Part4】03-事件详解
  8. Nginx服务安全加固
  9. layout_gravity与gravity的区别
  10. qrect在图片上显示矩形框_Mac上用LabelImg手动标记图片
  11. 计算机仿真技术与cad第三版课后答案,《计算机仿真技术与CAD习题答案》.doc
  12. 计算机在智能交通应用,计算机信息技术在智能交通系统中的应用
  13. windows默认打开计算机,[Answers 分享]如何在Windows 7中还原.dll文件的默认打开方式...
  14. 阅读笔记:利用Python进行数据分析第2版——第8章 数据规整:聚合、合并和重塑
  15. 如何用python画出自己喜欢的表情包
  16. windows 命令行 通过驱动名删除打印机
  17. 【WC2013】糖果公园 树上莫队
  18. 教女朋友学Python是道送命题
  19. 使用SQLMAP自动化探测SQL注入
  20. 【工作记录】支付系统数据库梳理

热门文章

  1. UIImageView 加入子UIImageView 导致内存泄露的问题
  2. android之weex之component插件开发
  3. 链栈判断回文 java_C语言链栈判断回文
  4. java 程序打包成jar_把Java程序打包成jar文件包并执行的方法
  5. MySQL的chap服务器_路由器chap的双向配置命令
  6. 连接黑屏_解决地下城与勇士PK服登录游戏卡在“正在连接服务器”的黑屏界面...
  7. icmp协议_计算机网络基础(七)网络层ICMP协议
  8. 进阶15 IO流+字节字符输入输出+IO异常处理+属性集+缓冲流+各种编码+序列化
  9. android listview 只加载显示的图片大小,Android ListView只加载当前屏幕内的图片(解决list滑动时加载卡顿)...
  10. 论文页眉奇偶页不同怎么设置_什么!论文排版这么简单的吗?!