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

​关于读写锁

前面的章节中咱们分析了Java语法层面的synchronized锁和JDK内置可重入锁ReentrantLock,咱们在多线程并发场景中能够经过它们来控制对资源的访问从而达到线程安全。这两种锁都属于纯粹的独占锁,也就是说这些锁任意时刻只能由一个线程持有,其它线程都得排队依次获取锁。算法

有些场景下为了提升并发性能咱们会对纯粹的独占锁进行改造,额外引入共享锁来与独占锁共同对外构成一个锁,这种就叫读写锁。为何叫读写锁呢?主要是由于它的使用考虑了读写场景,通常认为读操做不会改变数据因此能够多线程进行读操做,但写操做会改变数据因此只能一个线程进行写操做。读写锁在内部维护了一对锁(读锁和写锁),它经过将锁进行分离从而获得更高的并发性能。安全

以下图中,存在一个读写锁对象,其内部包含了读锁和写锁两个对象。假如存在五个线程,其中线程一和线程二想要获取读锁,那么两个线程是能够同时获取到读锁的。可是写锁就不能够共享,它是独占锁。好比线程3、线程四和线程五都想要持有写锁,那么只能一个个线程轮着持有。数据结构

读写锁

读写锁的性质

  • 能够多个线程同时持有读锁,某个线程成功获取读锁后其它线程仍然能成功获取读锁,即便该线程不释放读锁。多线程

读写锁性质1
  • 在某个线程持有读锁的状况下其它线程不能持有写锁,除非持有读锁的线程所有都释放掉读锁。并发

读写锁性质2
  • 在某个线程持有写锁的状况下其它线程不能持有写锁或读锁,某个线程成功获取写锁后其它全部尝试获取读锁和写锁的线程都将进入等待状态,只有当该线程释放写锁后才其它线程可以继续往下执行。app

读写锁性质3
  • 若是咱们要获取读锁则须要知足两个条件:目前没有线程持有写锁和目前没有线程请求获取写锁。机器学习

读写锁性质4
  • 若是咱们要获取写锁则须要知足两个条件:目前没有线程持有写锁和目前没有线程持有读锁。分布式

读写锁性质5

简单的实现版本

为了加深对读写锁的理解,在分析JDK实现的读写锁以前咱们先来看一个简单的读写锁实现版本。其中三个整型变量分别表示持有读锁的线程数、持有写锁的线程数以及请求获取写锁的线程数,四个方法分别对应读锁、写锁的获取和释放操做。acquireReadLock方法用于获取读锁,若是持有写锁的线程数量或请求读锁的线程数大于0则让线程进入等待状态。releaseReadLock方法用于释放读锁,将读锁线程数减一并唤醒其它线程。acquireWriteLock方法用于获取写锁,若是持有读锁的线程数量或持有写锁的线程数量大于0则让线程进入等待状态。releaseWriteLock方法用于释放写锁,将写锁线程数减一并唤醒其它线程。高并发

读写锁简单版本

读锁升级为写锁

在某些场景下,咱们但愿某个已经拥有读锁的线程可以得到写锁,并将原来的读锁释放掉,这种状况就涉及到读锁升级为写锁操做。读写锁的升级操做须要知足必定的条件,这个条件就是某个线程必须是惟一拥有读锁的线程,不然将没法成功升级。以下图中,线程二已经持有读锁了,并且它是惟一的一个持有读锁的线程,因此它能够成功得到写锁。工具

读锁升级

写锁降级为读锁

与锁升级相对应的是锁降级,锁降级就是某个已经拥有写锁的线程但愿可以得到读锁,并将原来的写锁释放掉。锁降级操做几乎没有什么风险,由于写锁是独占锁,持有写锁的线程确定是惟一的,并且读锁也确定不存在持有线程,因此写锁能够直接降级为读锁。以下图中,线程三持有写锁,此时其它线程不可能持有读锁和写锁,因此能够安全地将写锁降为读锁。

写锁降级

ReadWriteLock接口

ReadWriteLock其实是一个接口,它仅仅提供了两个方法:readLock和writeLock。分别表示获取读锁对象和获取写锁对象,JDK为咱们提供了一个内置的读写锁工具,那就是ReentrantReadWriteLock类,咱们将对其进行深刻分析。ReentrantReadWriteLock类包含的属性和方法较多,为了让分析思路清晰且方便读者理解,咱们将剔除非核心源码,只对核心功能进行分析。

ReentrantReadWriteLock三要素

ReentrantReadWriteLock类的三要素为:公平/非公平模式、读锁对象和写锁对象。其中公平/非公平模式表示多个线程同时去获取锁时是否按照先到先得的顺序得到锁,若是是则为公平模式,不然为非公平模式。读锁对象负责实现读锁功能,而写锁对象负责实现写锁功能,这两个类都属于ReentrantReadWriteLock的内部类,下面会详细讲解。

ReentrantReadWriteLock实现思想

总的来讲,ReentrantReadWriteLock类的内部包含了ReadLock内部类和WriteLock内部类,分别对应读锁和写锁,这两种锁都提供了公平模式和非公平模式。无论公平模式仍是非公平模式、不论是读锁仍是写锁都是基于AQS同步器来实现的。实现的主要难点在于只使用一个AQS同步器对象来实现读锁和写锁,这就要求读锁和写锁共用同一个共享状态变量,下面会具体讲解如何用一个状态变量来供读锁和写锁使用。

实现思想

对应ReentrantReadWriteLock类的结构以下,ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock分别为读锁对象和写锁对象。Sync对象表示ReentrantReadWriteLock类的同步器,它基于AQS同步器,而FairSync类和NonfairSync类分别表示公平模式和非公平模式的同步器,能够看到默认状况下使用的是非公平模式。

读写锁共用状态变量

前面提到过ReentrantReadWriteLock的难点在于读锁和写锁都共用一个共享变量,下面看具体是如何共用的。咱们知道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的两个要素,它们都属于ReentrantReadWriteLock的内部类。它们都实现了Lock接口,咱们主要关注lock、unlock和newCondition这几个核心方法。分别表示对读锁和写锁的加锁操做、释放锁操做和建立Condition对象操做,能够看到这些方法都间接调用了ReentrantReadWriteLock的同步器的方法,须要注意的是读锁不支持建立Condition对象。咱们在可重入锁ReentrantLock章节中已经讲解过Condition对象,本节将再也不赘述。

公平/非公平模式

ReentrantReadWriteLock的默认模式为非公平模式,其内部类Sync是公平模式FairSync类和非公平模式NonfairSync类的抽象父类。由于ReentrantReadWriteLock的读锁使用了共享模式,而写锁使用了独占模式,因此该父类将不一样模式下的公平机制抽象成readerShouldBlock和writerShouldBlock两个抽象方法,而后子类就能够各自实现不一样的公平模式。换句话说,ReentrantReadWriteLock的公平机制就由这两个方法来决定了。

下面看公平模式的FairSync类,该类的readerShouldBlock和writerShouldBlock两个方法都直接返回hasQueuedPredecessors方法的结果,这个方法是AQS同步器的方法,用于判断当前线程前面是否有排队的线程。若是有排队队列就要让当前线程也加入排队队列中,这样按照队列顺序获取锁也就保证了公平性。

继续看非公平模式NonfairSync类,该类的writerShouldBlock方法直接返回false,代表不要让当前线程进入排队队列中,直接进行锁的获取竞争。readerShouldBlock方法则调用apparentlyFirstQueuedIsExclusive方法,这个方法是AQS同步器的方法,用于判断头结点的下一个节点线程是否在请求获取独占锁(写锁)。若是是则让其它线程先获取写锁,而本身则乖乖去排队。若是不是则说明下一个节点线程是请求共享锁(读锁),此时直接与之竞争读锁。

公平/非公平

写锁WriteLock的实现

上面的介绍中咱们知道WriteLock有两个核心方法:lock和unlock。它们都会间接调用了ReentrantReadWriteLock内部同步器的对应方法,在同步器中须要重写tryAcquire方法和tryRelease方法,分别用于获取写锁和释放写锁操做。

先看tryAcquire方法的逻辑,获取状态值并经过exclusiveCount方法获得低16位的写锁状态值。c!=0时有两种状况,一种是高16位的读锁状态不为0,一种是低16位的写锁状态不为0。w等于0时表示还有线程持有读锁,直接返回false表示获取写锁失败。若是持有写锁的线程为当前线程,则表示写锁重入操做,此时须要将状态变量进行累加,此外须要校验的是写锁重入状态值不能超过MAX_COUNT。经过writerShouldBlock方法判断是否须要将当前线程放入排队队列中,同时经过拥有CAS算法的compareAndSetState方法对状态变量进行累加操做,CAS失败的话也须要将当前线程放入排队队列中。对于非公平模式,这里的CAS操做就是闯入操做,即线程先尝试一次竞争写锁。最后经过setExclusiveOwnerThread设置当前线程持有写锁,该方法只是简单的设置变量方法。

继续看tryRelease方法的逻辑,先用isHeldExclusively方法检查当前线程必须为写锁持有线程。而后将状态值减去释放的值,并经过exclusiveCount获得低16位的写锁状态值,若是其值为0则表示已经没有重入能够完全释放锁了,调用setExclusiveOwnerThread(null)设置没有线程持有写锁。最后设置新的状态值。

读锁ReadLock的实现

ReadLock一样有两个核心方法:lock和unlock。它们都会间接调用了ReentrantReadWriteLock内部同步器的对应方法,在同步器中须要重写tryAcquireShared方法和tryReleaseShared方法,分别用于获取读锁和释放读锁操做。

tryAcquireShared方法的逻辑为:先经过getState方法获取状态值,而后经过exclusiveCount方法获取低16位的写锁状态,若是不为0则表示有其它线程持有写锁并且当前线程没有持有写锁,则此时尝试获取读锁失败,返回-1,即将当前线程放到排队队列。注意这里若是当前线程持有写锁的话则能够继续获取读锁。继续经过sharedCount获得高16位的读锁,而后尝试用CAS算法设置新的状态值,若是成功则返回1表示成功获取读锁。若是不成功则继续调用fullTryAcquireShared方法。

fullTryAcquireShared方法的逻辑为:这是一个无限自旋操做,首先获取状态值,若是写锁不为0且当前线程不为持有写锁程序,则返回-1,表示尝试获取读锁失败,将当前线程加入排队队列中。若是写锁的状态为0,则表示没有线程持有写锁,继续经过readerShouldBlock方法判断是否须要将该线程加入到排队队列中,若是须要则返回-1,AQS同步器会将其加入到排队队列中。此外,读锁的状态值不能等于MAX_COUNT,即已经达到最大读锁数了。最后,经过CAS算法的compareAndSetState方法设置新的状态值,这里的for无限循环就是自旋,指经过自旋方式来竞争读锁。须要注意的是,在非公平模式下若是排队队列中下一个线程是要获取写锁,则这个自旋操做也会被打破。

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

例子

以下是一个读写锁的使用例子,咱们实例化了一个ReentrantReadWriteLock对象,而后经过它的读锁和写锁来控制对某个线程不安全的TreeMap对象的访问。咱们能够看到get方法属于读取数据的操做,因此使用共享的读锁便可。而put和clear两个方法涉及到修改数据的操做,须要使用独占的写锁。

例子

总结

本文介绍了Java中的读写锁ReentrantReadWriteLock,从名字上看就知道它具备可重入性且提供了读写锁的功能。咱们讲解了它的核心三要素以及实现原理。在ReentrantReadWriteLock读写锁中,写锁是一种独占锁,包括了公平模式和非公平模式。而读写则是一种共享锁,它也包含了公平模式和非公平模式。ReentrantReadWriteLock类的实现基于AQS同步器,其中最重要的点是它经过某些技巧让读锁和写锁公共了同一个状态变量,高16位与低16位。经过本文的讲解相信你们已经很好地掌握了JDK提供的读写锁的实现原理。

专一于人工智能、读书与感想、聊聊数学、计算机科学、分布式、机器学习、深度学习、天然语言处理、算法与数据结构、Java深度、Tomcat内核等。

相关文章
相关标签/搜索