前言

本篇适用于了解ReentrantLock或ReentrantReadWriteLock的使用,但想要进一步了解原理的读者。见于之前的分析都是借鉴大量的JDK源码,这次以流程图的形式代替源码,希望读者能有更好的阅读体验。有兴趣了解源码的读者也可以借鉴本篇的分析成果做源码分析。

《从源码分析ReentrantLock原理》这一篇文章中分析了以非阻塞同步算法为基础实现的可重入独占锁ReentrantLock。所谓** “独占” 即同一时间只能有一个线程持有锁。而 “重入” **是指该线程如果持有锁,可以在同步代码块内再次请求占有锁而不被阻塞,线程重入后将AQS内部状态state同步加1继续同步区的操作。但是要注意该线程要想移交锁的控制权必须完全释放重入锁,即将AQS的state同步更新到0为止。

ReentrantReadWriteLock出现的目的就是针对ReentrantLock独占带来的性能问题,使用ReentrantLock无论是“写/写”线程、“读/读”线程、“读/写”线程之间的工作都是互斥,同时只有一个线程能进入同步区域。然而大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock,它的特性是:** 一个资源可以被多个读操作访问,或者一个写操作访问,但两者不能同时进行。**从而提高读操作的吞吐量。

初识ReentrantReadWriteLock

ReentrantReadWriteLock并没有继承ReentrantLock,也并没有实现Lock接口,而是实现了ReadWriteLock接口,该接口提供readLock()方法获取读锁,writeLock()获取写锁。

public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable { private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock; public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
} public interface ReadWriteLock { Lock readLock(); Lock writeLock();
}

默认构造方法为** 非公平模式 ,开发者也可以通过指定fair为true设置为 公平模式 **。

    public ReentrantReadWriteLock() {
this(false);
} public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
} public static class ReadLock implements Lock, java.io.Serializable {}
public static class WriteLock implements Lock, java.io.Serializable {}

而公平模式和非公平模式分别由内部类FairSync和NonfairSync实现,这两个类继承自另一个内部类Sync,该Sync继承自AbstractQueuedSynchronizer(以后简称** AQS **),这里基本同ReentrantLock的内部实现一致。

abstract static class Sync extends AbstractQueuedSynchronizer {
} static final class FairSync extends Sync {
} static final class NonfairSync extends Sync {
}

在ReentrantLock的分析中得知,其独占性和重入性都是通过CAS操作维护AQS内部的state变量实现的。ReentrantReadWriteLock将这个int型state变量分为高16位和低16位,高16位表示当前读锁的占有量低16位表示写锁的占有量,详见ReentrantReadWriteLock的内部类Sync :

abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; /** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
...
}

读锁分析

读锁,锁定的是AQS的state变量的高16位,当state的高16位等于0,表示当前读锁未被占有;当state的高16位大于0,表示当前读锁可能被一个或多个线程占有,多于一个占有读锁的线程,允许重入。

读锁竞争

 
读锁竞争过程.png

读锁的获取条件要满足:

  1. ** 当前的写锁未被占有(AQS state变量低16位为0) 或者当前线程是写锁占有的线程**
  2. ** readerShouldBlock()方法返回false **
  3. ** 当前读锁占有量小于最大值(2^16 -1) **
  4. ** 成功通过CAS操作将读锁占有量+1(AQS的state高16位同步加1) **

条件1使得读锁与写锁互斥,除非当前申请读操作的线程是占有写锁的线程,即实现了写锁降级为读锁。

条件2在非公平模式下执行的是NonfairSync类的readerShouldBlock()方法:

    final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
} final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}

如果AQS的锁等待队列head节点后的节点非共享节点(等待读锁的节点),将返回true。

条件2在公平模式下执行的是FairSync类的readerShouldBlock方法:

    final boolean readerShouldBlock() {
return hasQueuedPredecessors();
} public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

只要AQS锁等待队列的头尾不为空,并且存在head后的节点并且节点的线程非当前线程,返回true。

条件3保证读锁的占有数不超过最大上限,条件4保证多线程竞争读锁时的安全性。

不满足条件申请读锁的线程会被封装为SHARED类型的线程节点插入到AQS锁等待队列的末尾,在插入队列尾后还有一次机会尝试获取读锁。如果还是失败的,下面判断如果队列前一节点是SIGNAL状态就将线程挂起。当线程唤醒后会再次尝试获取读锁,不满足条件会再次挂起,以此循环。

** 如果在线程挂起前获取读锁,下面会将当前节点设置为head节点,并将head后的SHARED类型的节点的唤醒。然后进入读锁同步区域。被唤醒的线程会继续尝试获取读锁,获取读锁成功后就继续上述步骤,这样就保证了队列中几个连续的等待读锁的线程被依次唤醒进入读锁同步区。**

读锁释放

 
读锁的释放.png

读锁的释放过程即AQS的state高16位同步递减为0的过程,当state的高16位都为0表示读锁释放完毕,如果此时写锁状态为0(即该读锁不是写锁降级来的),唤醒head节点后下一个SIGNAL状态的节点的线程,一般为等待写锁的节点。如果读锁的占有数不为0,表示读锁未完全释放。或者写锁的占有数不为0,表示释放的读锁是写锁降级来的。

写锁分析

写锁的状态表示为AQS的state变量的低16位,当state低16位为0,表示当前写锁没有被占有,反之表示写锁被某个写线程占有(state = 1)或重入(state > 1)。

写锁竞争

 
写锁竞争过程.png

写锁获取的条件需要满足:

  1. ** 读锁未被占用(AQS state高16位为0) ,写锁未被占用(state低16位为0)或者占用写锁的线程是当前线程**
  2. ** writerShouldBlock()方法返回false,即不阻塞写线程 **
  3. ** 当前写锁占有量小于最大值(2^16 -1),否则抛出Error("Maximum lock count exceeded") **
  4. ** 通过CAS竞争将写锁状态+1(将state低16位同步+1) **

条件1使得写锁与读锁互斥,ReentrantReadWriteLock并没有读锁升级的功能。

条件2的writerShouldBlock()方法在非公平模式下实现为:

        final boolean writerShouldBlock() {
return false; // writers can always barge
}

即非公平模式下允许满足条件的写操作直接插队。

条件2的writerShouldBlock()方法在公平模式下实现为:

        final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}

公平模式下同读锁一样,如果AQS的锁等待队列不为空,写操作无法插队。

条件3保证写锁占有线程的重入次数不会溢出上限,条件4保证多个写操作的线程竞争写锁的安全性。

不满足获取写锁条件的线程会封装为EXECLUSIVE型的NODE插入到AQS的锁等待队列尾部,通过acquireQueued方法进入循环,该循环内再次尝试获取写锁(因为经过上述操作,另一个锁占有线程可能释放了锁),否则通过shouldParkAfterFailedAcquire方法将前一节点设置为SIGNAL状态后将自身线程挂起。当线程被唤醒后会再次尝试获取写锁,失败则继续挂起,以此循环。或成功占有写锁则将当前Node设置为head节点,返回中断标记并进入同步代码区。** 与读操作不同的是写操作之间是互斥的,所以获取写锁后不会将下一个申请写操作的节点唤醒。**

写锁释放

 
写锁的释放.png

写锁的释放过程即AQS的state低16位同步递减为0的过程,当state的高16位都为0表示写锁释放完毕,唤醒head节点后下一个SIGNAL状态的节点的线程。如果该写锁占有线程未释放写锁前还占用了读锁,那么写锁释放后该线程就完全转换成了读锁的持有线程。

小结

  • 读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
  • 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
  • 对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
  • 公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
  • 读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。

作者:Mars_M
链接:https://www.jianshu.com/p/9f98299a17a5
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

可重入读写锁ReentrantReadWriteLock基本原理分析的更多相关文章

  1. java 可重入读写锁 ReentrantReadWriteLock 详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt206 读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只 ...

  2. 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁

    上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和基本的方法,显示了怎样 ...

  3. Java基础-Java中的并法库之重入读写锁(ReentrantReadWriteLock)

    Java基础-Java中的并法库之重入读写锁(ReentrantReadWriteLock) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 在学习Java的之前,你可能已经听说过读 ...

  4. 聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁

    这篇讲讲ReentrantReadWriteLock可重入读写锁,它不仅是读写锁的实现,而且支持可重入性. 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁) 这篇讲了怎样模拟一个读写锁. 可重 ...

  5. 【java并发编程】ReentrantLock 可重入读写锁

    目录 一.ReentrantLock可重入锁 二.ReentrantReadWriteLock读写锁 三.读锁之间不互斥 欢迎关注我的博客,更多精品知识合集 一.ReentrantLock可重入锁 可 ...

  6. 018-并发编程-java.util.concurrent.locks之-ReentrantReadWriteLock可重入读写锁

    一.概述 ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程.写线程和写线程同时访问.相对 ...

  7. 【原创】读写锁ReentrantReadWriteLock原理分析(一)

    Java里面真正意义的锁并不多,其实真正的实现Lock接口的类就三个,ReentrantLock和ReentrantReadWriteLock的两个内部类(ReentrantReadWriteLock ...

  8. 三、curator recipes之共享的可重入读写锁

    简介 curator实现了跨JVM的可重入读写互斥锁.它使用zookeeper去进行加锁,所以指定相同路径的处理线程将会基于“公平锁”的机制去竞争锁资源. 读写锁包含了读锁.写锁两个,它们的互斥关系如 ...

  9. 并发编程-concurrent指南-ReadWriteLock-ReentrantReadWriteLock(可重入读写锁)

    几个线程都申请读锁,都能获取: import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantRea ...

随机推荐

  1. 【BZOJ】3456: 城市规划(多项式求ln)

    题解 在我写过分治NTT,多项式求逆之后 我又一次写了多项式求ln 我们定义一个数列的指数型生成函数为 \(\sum_{i = 0}^{n} \frac{A_{i}}{i!} x^{i}\) 然后这个 ...

  2. bzoj [ZJOI2008]生日聚会Party

    思路:dp, 用dp[ i ][ j ][ u ][ v ] 表示, 有n个人,其中有j个是男生,后缀区间中男生人数减去女生人数的最大值为u, 女生人数减去男生人数 的最大值为v, 然后就能写出状态转 ...

  3. Zookeeper项目开发环境搭建(Eclipse\MyEclipse + Maven)

    写在前面的话 可详细参考,一定得去看 HBase 开发环境搭建(Eclipse\MyEclipse + Maven) 我这里,相信,能看此博客的朋友,想必是有一定基础的了.我前期写了大量的基础性博文. ...

  4. Android 短信拦截及用途分析

    监听系统短信这个只能作为一个技术点来研究下,读者可能在工作中可能不会哦涉及到,一般的应用软件也不会有这个需求 但是作为程序员呢,多了解一下也是好的. Android 监听系统短信有什么用? 1.对系统 ...

  5. queue模块回顾

    queue queue是python中的标准库,俗称队列. 在python中,多个线程之间的数据是共享的,多个线程进行数据交换的时候,不能够保证数据的安全性和一致性,所以当多个线程需要进行数据交换的时 ...

  6. 【莫队算法】【权值分块】bzoj3920 Yuuna的礼物

    [算法一] 暴力. 可以通过第0.1号测试点. 预计得分:20分. [算法二] 经典问题:区间众数,数据范围也不是很大,因此我们可以: ①分块,离散化,预处理出: <1>前i块中x出现的次 ...

  7. MVVM模式下关闭窗口的实现

    通过行为来实现 实现界面与逻辑的分离 窗口关闭行为:其中含有布尔型的Close属性,将相应的关闭行为绑定到该属性上,则可以实现窗口的关闭行为,从而实现VM与View的分离 public class W ...

  8. BZOJ1171 : 大sz的游戏

    f[i]=min(f[j])+1,线段j与线段i有交,且l[i]-l[j]<=L. 线段j与线段i有交等价于y[j]>=x[i],x[j]<=y[i]. 因为l[i]递增,所以可以维 ...

  9. CTSC被虐记

    退役前写写破事乐呵乐呵..(雾 Day0 愉快的没有分到另一个宾馆...但是是个单间...而且居然是大床房...难以置信, 试机向BeiYe学习了一发Gedit的外部工具, 试到一般好像都走了..只剩 ...

  10. java高新技术

    一.静态导入: import static语句导入一个类中的某个静态方法或所有方法: 例子: 1.import static java.lang.Math.max; 只是导入了Math类中的max方法 ...