Java多线程——ReentrantReadWriteLock源码阅读

之前讲了《AQS源码阅读》《ReentrantLock源码阅读》,本次将延续阅读下ReentrantReadWriteLock,建议没看过之前两篇文章的,先大概了解下,有些内容会基于之前的基础上阅读。
这个并不是ReentrantLock简单的升级,而是落地场景的优化,我们来详细了解下吧。

背景

JUC包里面已经有一个ReentrantLock了,为何还需要一个ReentrantReadWriteLock呢?看看头注解找点线索。

它是ReadWriteLock接口的实现。那看看这个接口怎么说
<!-- more -->
在实际场景中,一般来说,读数据远比写数据要多。如果我们还是用独占锁去锁线程避免线程不安全的话,是非常低效的,而且同时也会失去它的并发性。多线程也没有意义了。所以ReadWriteLock就是解决这个问题所存在的。
看回ReentrantReadWriteLock的头注解。

ReentrantReadWriteLock依然有公平锁/非公平锁的功能,与ReentrantLock不同在于,前者内部维护了读锁和写锁,在公平/非公平模式下,他们会一起去竞争这个锁资源。

上图是两条ReentrantReadWriteLock最核心的规则。

  1. 申请读锁。当没有其他写锁占有,或者读锁在队列中排队时间最长的,才能成功
  2. 申请写锁。当没有其他线程占有读/写锁的情况下,才能成功

又以上两条规则可以推导出,

  1. 写锁比读锁要高级
  2. 有读锁占用可以继续申请读锁,但其他线程不能申请写锁
  3. 有写锁占用其他线程读写都不能申请

所以扣ReadWriteLock接口的说明,可以让读并发,写独占,提高了程序的并发性。

ReentrantReadWriteLock构成

看下ReentrantReadWriteLock的file struture

之前看过ReentrantLock源码的同学肯定很熟悉这个结构,看起来相同的都是Sync同步器(AQS的子类),以及它的两个公平/非公平子类。
不同的是它还多了ReadLock内部类和WriteLock内部类,以及读写对应的成员变量和方法。并且少了lock()、unlock()等方法,而是把加锁解锁的功能下方给这两个子类,符合ReadWriteLock接口的定义。

Sync内部类

虽然ReentrantReadWriteLock和ReentrantLock都有Sync,但其实Sync方法已经很大不同了,看下Sync的结构

对比之前ReentrantLock的Sync,最大不同在于它多了**shared()方法,用于共享锁的获取与释放。
另外tryReadLock()、tryWriteLock()是给WriteLock和ReadLock内部类使用的。

tryAcquire() 独占锁(写锁)申请


上文介绍重入锁说到state代表的是重入的次数,在读写锁的语义下,state代表的读/写占有(重入)的次数。c为state,w为独占重入次数。
当有线程占用锁时(c!=0),如果没有写锁(w==0)或者独占线程不是当前线程,返回false获取失败。锁的重入总数超过上限会抛出异常。
这里很容易看出来,如果有锁占用的时候,如果只是读锁,依然可以申请成功。这就是读锁的锁升级
当没有线程占用的时候,执行writerShouldBlock()判断是否需要阻塞线程(子类实现自己的条件),不需要则CAS state值,返回成功。

tryAquireShared() 共享锁(读锁)申请


读锁申请比写锁申请要复杂,有比较多没接触过的成员变量,判断的语句也比较多。
先看看成员变量,从他们各自的变量注解可知

  • firstReader,是第一个获取读锁的线程
  • firstReaderHoldCount,是firstReader的计数器。
  • cachedHoldCounter,最近一个成功获取读锁的线程持有数计数器。
  • readHolds,当前线程重入读锁次数。ThreadLocal<HoldCounter>,是线程安全的HoldCounter

先判断是否有写锁占有,如果写锁不是当前线程,获取读锁失败,退出方法。
注意如果写锁是当前线程是可以获取读锁的,因为写锁是独占的,这种情况下是不会有数据与其他线程共享的问题。
满足子类条件,也不超过总数,CAS也成功的情况下,
如果没有读锁,则设firstReader为当前线程,firstReaderHoldCount为1;
如果有读锁,并且也是当前线程申请获取,firstReaderHoldCount自增1;
如果有读锁,不是当前线程申请,取上一个成功的缓存计数器,如果这个计数器不是当前线程的,则设为当前的计数器,并且自增,返回成功。(其实就是把缓存计数器置换为当前线程的计数器)
最后不满足条件或者CAS失败,执行fullTryAcquireShared(current)返回。
至于这些数据算来干嘛,等后面看看release()怎么用。

其实这个方法就是用for循环轮询解决CAS丢失和重入失败的问题,具体代码不细过了,有兴趣可以自己找源码看看。

tryRelease() 独占锁(写锁)释放


这里又有Condition的踪迹了,大概可以才行到Condition时控制锁的行为的,取消唤醒等操作。
另外锁会同时释放读锁和写锁。
这个方法比较好理解的,只要是当前线程操作下,持有重入数减去释放数为0就可以释放了,否则失败。

tryReleaseShared() 共享锁(读锁)释放


释放读锁,对正在读的线程不会有什么影响,但可以让等待的写线程去开始获取写锁。
剩余的内容就是对tryAquireShared()计算的count数值进行释放(自减),如果最终自减为0则释放读锁成功。

WriteLock、ReadLock内部类

前面说到ReentrantReadWriteLock的lock()、unlock()操作是分配到Write/ReadLock里面执行的。
他们都是Lock接口的实现,所以其实最像ReentrantLock应该是这个两个内部类。而且大体上也没什么差异,也是用Sync的内部类。
WriteLock、ReadLock最大的不同就是WriteLock用的独占模式的方法,ReadLock用的是共享模式的方法。
具体的代码实现基本就是上面说明的组成,下面介绍下ReentranReadWriteLock的使用。
ReentrantLock的时候比较简单,声明一个变量,调用lock()方法即可。

    ReentrantLock rl = new ReentrantLock();
    rl.lock();
    rl.unlock();

但ReentranReadWriteLock并不是Lock接口的实现,所以没有这些方法。
有的只是writeLock()、readLock(),要先调用这个方法获取应对的锁对象,再调用lock()。

     ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     rwl.readLock().lock();
     rwl.readLock().unlock();
     rwl.writeLock().lock();
     rwl.writeLock().unlock();

总结

回顾下要点

  1. 读写锁ReentrantReadWriteLock,是基于多读少写的实际场景,提高并发性
  2. 读写锁的Sync添加了共享模式的方法
  3. 读写锁内置了两个对象readLock、writeLock,用于实际的加锁解锁
  4. 写锁是独占的,不允许其他锁的申请
  5. 读锁可以并发重复申请,当有写锁的时候,会发生锁升级

特别地,在此祝福8月27日生日的她。



更多技术文章、精彩干货,请关注
博客:zackku.com
微信公众号:Zack说码

原文地址:http://blog.51cto.com/14073604/2317964

时间: 2024-10-05 04:58:52

Java多线程——ReentrantReadWriteLock源码阅读的相关文章

Java集合框架源码阅读之AbstractCollection

AbstractCollection是集合实现类的根抽象实现类,它实现了Collection接口,集合中的三个分支Set.List.Queue都是继承此类之后再进行各自实现的扩展,分别是AbstractSet.AbstractList.AbstractQueue.这三个分支有一些共同之处,需要用一些共同的方法,因此出现了AbstractCollection类,它包含了一些这三个分支都会用到的常用方法.而这三个分支也各有抽象类,因为这三个分支下面的一些具体实现也会有一些当前分支通用的方法,因此也给

【转】Java开源项目源码阅读方法及二次开发方法

一直以来,都想要阅读某些Java开源项目的源代码,甚至想要修改某些代码,实现对开源项目进行二次开发的目的.但总是不知从何入手,直接将开源项目的源代码导入Eclipse,总是会报很多错误,而无法编译.可以直接通过Eclipse打开开源项目的源代码,至少能够达到可视化源码阅读.源码导航的目的,还是能在一定程度上解决源码阅读不爽的问题,因为直接打开并没有改变源文件项目的目录结果,对于修改过后的代码,可以通过命令行找到源文件项目目录,并使用mvn或者ant对项目进行编译,再查看修改后的项目是否正确. 由

Java线程池源码阅读

简单介绍 线程池是池化技术的一种,对线程复用.资源回收.多任务执行有不错的实践.阅读源码,可以学习jdk的大师对于线程并发是怎么池化的,还有一些设计模式.同时,它也能给我们在使用它的时候多一种感知,出了什么问题可以马上意识到哪里的问题. 使用范例 我们使用一个线程池,直接通过jdk提供的工具类直接创建.使用如下api创建一个固定线程数的线程池. ExecutorService pool = Executors.newFixedThreadPool(5); 使用如下api创建一个会不断增长线程的线

《java.util.concurrent 包源码阅读》03 锁

Condition接口 应用场景:一个线程因为某个condition不满足被挂起,直到该Condition被满足了. 类似与Object的wait/notify,因此Condition对象应该是被多线程共享的,需要使用锁保护其状态的一致性 示例代码: class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition

Java Jdk1.8 HashMap源码阅读笔记一

最近在工作用到Map等一系列的集合,于是,想仔细看一下其具体实现. 一.结构 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 1.抽象类AbstractMap public abstract class AbstractMap<K,V> implements Map<K,V> 该类实现了Map接口,具体结

《java.util.concurrent 包源码阅读》09 线程池系列之介绍篇

concurrent包中Executor接口的主要类的关系图如下: Executor接口非常单一,就是执行一个Runnable的命令. public interface Executor { void execute(Runnable command); } ExecutorService接口扩展了Executor接口,增加状态控制,执行多个任务返回Future. 关于状态控制的方法: // 发出关闭信号,不会等到现有任务执行完成再返回,但是现有任务还是会继续执行, // 可以调用awaitTe

《java.util.concurrent 包源码阅读》04 ConcurrentMap

Java集合框架中的Map类型的数据结构是非线程安全,在多线程环境中使用时需要手动进行线程同步.因此在java.util.concurrent包中提供了一个线程安全版本的Map类型数据结构:ConcurrentMap.本篇文章主要关注ConcurrentMa接口以及它的Hash版本的实现ConcurrentHashMap. ConcurrentMap是Map接口的子接口 public interface ConcurrentMap<K, V> extends Map<K, V> 与

Java源码阅读

源码阅读目的是为了了解Java原理,学习优秀的类设计,整体阅读顺序和侧重主要参考基础类和常用类,参考网上整体归纳如下: 包 java.lang 1) Object 1 2) String 1 3) AbstractStringBuilder 1 4) StringBuffer 1 5) StringBuilder 1 6) Boolean 2 7) Byte 2 8) Double 2 9) Float 2 10) Integer 2 11) Long 2 12) Short 2 13) Thr

如何阅读Java源码 阅读java的真实体会

刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 说到技术基础,我打个比方吧,如果你从来没有学过Java,或是任何一门编程语言如C++,一开始去啃<Core Java>,你是很难从中吸收到营养的,特别是<深入Java虚拟机>这类书,别人觉得好,未必适合现在的你. 虽然Tomcat的源码很漂亮,但我绝不建议你一开始就读它.我文中会专门谈到这个,暂时不展开. 强烈