前言

在传统的对共享变量的操作过程中,无论是对其进行读操作还是写操作,都会进行加锁.对于写操作而言加锁的意义就比较明显因为写操作要修改共享变量这就需要不同线程之间的互斥.但是读操作却不涉及到修改操作,仅仅是把共享变量加载到线程的工作内存中.AQS中的ReenReadWriteLock内部实现了读读并发,读写/写读,写写互斥.

源码解读

ReenReadWriteLock对于AQS内部维护的state跟ReentrantLock略有不同,state被切割为了两部分,一个int类型的变量占4个字节,1个字节占8个长度.state将高16位表示共享锁(读锁),低16位表示独占锁(写锁).那么为什么要这样设计呢?为何不把两个模式的加锁设成两个变量?

个人认为有以下几个原因:
1.空间利用率方面.一把锁的状态不大可能超过2的16次幂.也就是65536.无论是读锁还是写锁也好.几率可以说非常小.如果用32位来表示,可想而知空间利用率多么地低下.
2.时间效率方面.这两种状态是密不可分的,也就是说两种状态存在一定的依赖性.典型的例子就是锁降级.加载两个状态值的时间花费肯定是大于加载一个状态值的.每次从内存中读取另一个状态值也是需要消耗时间的.位运算是要比从内存中读取快很多的.

两种状态值的获取方式:
独占锁:将state与1左移 16位做与运算.
共享锁:直接将state右移16位

        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; }

1.writeLock

        protected final boolean tryAcquire(int acquires) {/** Walkthrough:* 1. If read count nonzero or write count nonzero*    and owner is a different thread, fail.* 2. If count would saturate, fail. (This can only*    happen if count is already nonzero.)* 3. Otherwise, this thread is eligible for lock if*    it is either a reentrant acquire or*    queue policy allows it. If so, update state*    and set owner.*/// 1.获取当前线程Thread current = Thread.currentThread();// 2.获取锁的状态int c = getState();// 3.获取独占锁的数量int w = exclusiveCount(c);// 4.判断当前锁是不是自由状态(没有读锁也没有写锁)if (c != 0) {// 5.当前锁被人持有// 6.当前锁的状态是共享锁还是独占锁// 如果是共享锁,判断当前上锁的线程是否是自己// 注意:这里是一个逻辑或运算,如果第一个条件为true,会发生短路.// 第二个条件不会进行判断// 场景一:如果没有人来进行独占锁加锁,如果当前线程来加锁.意味着要锁升级,// 这是不允许的.需要等待读锁释放完毕才可以进行加锁// 这可能会导致脏数据的产生,另外也违背了互斥性原则// 场景二:锁被人持有,但是w不为0,说明有人在上独占锁,第一个条件为false// 接着走第二个条件,当前锁的持有者是不是自己,如果是自己,则进行锁的重入.// 如果不是自己,那么遵循互斥性原则的写写互斥.当前线程要加锁失败// (Note: if c != 0 and w == 0 then shared count != 0)if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}// 7.该方法在非公平模式下,直接返回的是false.公平锁模式下是判断是否有队列形成if (writerShouldBlock() ||// 8. CAS加锁失败直接返回!compareAndSetState(c, c + acquires))return false;// 9.将当前持有者线程设为自己setExclusiveOwnerThread(current);return true;}

获取失败后进行入队.写锁的上锁模式相比于ReentrantLock还是比较容易理解的.需要注意的是锁升级的禁止.接下来我们来重点看下Doug Lea是如何做到读读并发的.在未看源码之前,我们可以自己设想一下,我们自己应该如何设计.

2.readLock

        protected final int tryAcquireShared(int unused) {/** Walkthrough:* 1. If write lock held by another thread, fail.* 2. Otherwise, this thread is eligible for*    lock wrt state, so ask if it should block*    because of queue policy. If not, try*    to grant by CASing state and updating count.*    Note that step does not check for reentrant*    acquires, which is postponed to full version*    to avoid having to check hold count in*    the more typical non-reentrant case.* 3. If step 2 fails either because thread*    apparently not eligible or CAS fails or count*    saturated, chain to version with full retry loop.*/// 1.获取当前线程Thread current = Thread.currentThread();// 2.获取锁的状态int c = getState();// 3.判断当前锁的状态是否为独占锁,如果是独占锁则判断独占锁是否是自己.// 如果当前状态是独占锁,并且持有者线程不是自己,直接返回-1执行入队操作// 写写互斥if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;// 4. 获取共享锁int r = sharedCount(c);// 5.该方法判断了队列是否形成,并且head的下一个节点是否是独占锁,// 因为如果head节点下一个节点是独占锁的话,!readerShouldBlock()则是false,// 这里顺着代码分析,如果head节点的下一个节点是共享锁,则判断当前的读锁是否小于// 最大读锁.接着CAS加锁if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {// 6.如果队列没有形成,并且当前线程是第一次来进行加锁.// firstReader 置为当前线程.这里设计的目的是,当这个线程重入时,// 可以直接进行加锁,有点像sync的偏向锁if (r == 0) {firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {// 7.如果当前当前线程不是第一次加锁,第二次进行加锁,是锁的重入.// 则firstReaderHoldCount++firstReaderHoldCount++;} else {// 8.如果锁的状态是被人持有共享锁.进入以下判断// 9.这里的HoldCounter只有两个属性// 一个是count一个是tid(这个tid是根据偏移量计算出来的一个值)// HoldCounter是用来统计每个线程的共享锁的上锁次数的.HoldCounter rh = cachedHoldCounter;// 如果当前线程没有缓存过,短路// 直接进行存储,readHolds底层是一个ThreadLocal// key是Thread value是int类型的countif (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}return fullTryAcquireShared(current);}
    final boolean apparentlyFirstQueuedIsExclusive() {Node h, s;return (h = head) != null &&(s = h.next)  != null &&!s.isShared()         &&s.thread != null;}

写锁的释放可以参考<AQS之ReentrantLock>在此不再赘述
https://blog.csdn.net/Cover_sky/article/details/117737555
接下来我们再来看下读锁是如何释放的

        protected final boolean tryReleaseShared(int unused) {// 1.获取当前线程Thread current = Thread.currentThread();// 2.判断当前线程是否是第一个加共享锁的线程if (firstReader == current) {// 3.如果是第一个加共享锁的线程,并且只添加过一次// 那么直接将firstReader 置为NULL// 反之则自减// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} else {// 4.如果来释放共享锁的线程不是第一个来加共享锁的线程// 获取最后获取共享锁的线程信息// 如果当前线程不是最后一个加共享锁的线程// 则从readHolds中获取该线程对应的ThreadLocalHoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();// 5.获取该线程对应的共享锁次数int count = rh.count;// 6.判断是否小于等于1,if (count <= 1) {// 7.小于等于1 则删除该线程对应的ThreadLocalreadHolds.remove();// 8. 并发场景下移除ThreadLocal将直接抛出异常if (count <= 0)throw unmatchedUnlockException();}// 9.该线程曾多次添加共享锁,进行自减--rh.count;}// 10.以上操作只是把该线程对应的加锁次数进行了修改.state还没有进行修改// 下方是CAS修改state的操作for (;;) {int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))// Releasing the read lock has no effect on readers,// but it may allow waiting writers to proceed if// both read and write locks are now free.return nextc == 0;}}

AQS之ReenReadWriteLock相关推荐

  1. java aqs源码_Java-AQS源码详解(细节很多!)

    ReentrantLock调用lock()时时序图: addWaiter方法: enq方法:自旋 它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用 ...

  2. java8 同步队列_秋招之路8:JAVA锁体系和AQS抽象队列同步器

    整个的体系图 悲观锁,乐观锁 是一个广义概念:体现的是看待线程同步的不同角度. 悲观锁 认为在自己使用数据的时候一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不被别的线程修改. 实现:关 ...

  3. Java中的锁原理、锁优化、CAS、AQS详解

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:景小财 www.jianshu.com/p/e674ee68 ...

  4. JUC AQS ReentrantLock源码分析

    Java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还 ...

  5. java多线程aqs实现工具类_Java并发多线程 - 并发工具类JUC

    (adsbygoogle = window.adsbygoogle || []).push({}); 安全共享对象策略 1.线程限制 : 一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改 ...

  6. Java并发同步器AQS

    AQS是AbstractQueuedSynchronizer的简写,中文名应该叫抽象队列同步器(我给的名字,哈哈),出生于Java 1.5. 一.什么是同步器 多线程并发的执行,之间通过某种 共享 状 ...

  7. JAVA那点破事!并发、IO模型、集合、线程池、死锁、非阻塞、AQS....

    关于Java面试,面试官一般喜欢问哪些问题? 本文对一些高频问题做了汇总,为了便于大家查找问题,了解全貌,整理个目录,我们可以快速全局了解关于 JAVA 接下来,我们逐条来看看每个问题及答案 JDK. ...

  8. 扔掉源码,15张图带你彻底理解java AQS

    java中AQS是AbstractQueuedSynchronizer类,AQS依赖FIFO队列来提供一个框架,这个框架用于实现锁以及锁相关的同步器,比如信号量.事件等. 在AQS中,主要有两部分功能 ...

  9. 源码级深挖AQS队列同步器

    我们知道,在java中提供了两类锁的实现,一种是在jvm层级上实现的synchrinized隐式锁,另一类是jdk在代码层级实现的,juc包下的Lock显示锁,而提到Lock就不得不提一下它的核心队列 ...

  10. 借鉴AQS的CHL思路解决消息多线程消费顺序ACK问题

    背景 我们的支付场景下,要求消费的业务消息绝不能丢失,且能充分利用高规格的服务器的性能,比如用线程池对业务消息进行快速处理.有同学可能没太理解这个问题有啥不好处理,让我一步步分析下. MQ的优势和缺点 ...

最新文章

  1. Sci-Hub重生了,这回用上了分布式网络
  2. [PAT乙级]1030 完美数列
  3. 忽悠CTO搞中台,把自己饭碗都搞砸了
  4. 百度面试题:malloc/free 与 new/delete 的区别
  5. panda是删除行_如何从Pandas数据帧中删除行列表?
  6. 计算几何——poj1410,线段不规范交
  7. java常用的库_java有哪些常用的库
  8. 2021-07-20 诺瓦星云笔试复盘
  9. Python自制日常办公辅助工具之:批量视频截图,子集固定尺寸截图+序列化命名
  10. XMind 超强入门完全指南
  11. Hexo博客之速度优化
  12. 2019-03-02 致虚极守静笃 读老子《道德经》有感
  13. 阿里云DataV使用笔记
  14. oracle expdp parallel,关于EXPDP中的PARALLEL参数
  15. canvas小虫子(利用canvas形成多个形状类似虫子的线条)
  16. 【拼图游戏】自选图片拼图--基于pygame实现
  17. android 获取权限管理,Android获取超级管理员权限的实现
  18. spss菜单小介绍【跟阿婷一起学spss 03 在入坑边缘疯狂试探】
  19. FPGA中高时钟频率计数器设计
  20. 小鸟云:浅谈5 种典型的云原生架构反模式

热门文章

  1. tensorflow-serving docker模型部署(以mnist为例)
  2. 不要悲观!勇敢面对逆境
  3. HttpClient请求https类型的网站接口碰到ssl证书不受信任问题处理
  4. 纪念半个月的旷课生活~
  5. etc 文件夹下放什么内容
  6. win7字体_win7系统为桌面添加自己喜欢的文字作为桌面背景,学习一下
  7. html手机陀螺儀页面,详解html5如何获取手机陀螺仪角度信息的示例代码
  8. lighttpd 之九 配置信息加载
  9. 戴尔R230安装esxi
  10. 使用Match函数对合并单元格的行数进行统计