在了解了AQS独占锁模式以后,接下来再来看看共享锁的实现原理。

原文地址:http://www.jianshu.com/p/1161d33fc1d0

搞清楚AQS独占锁的实现原理之后,再看共享锁的实现原理就会轻松很多。两种锁模式之间很多通用的地方本文只会简单说明一下,就不在赘述了,具体细节可以参考我的上篇文章深入浅出AQS之独占锁模式

一、执行过程概述

获取锁的过程:

  1. 当线程调用acquireShared()申请获取锁资源时,如果成功,则进入临界区。
  2. 当获取锁失败时,则创建一个共享类型的节点并进入一个FIFO等待队列,然后被挂起等待唤醒。
  3. 当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的所有共享节点,然后进入临界区,否则继续挂起等待。

释放锁过程:

  1. 当线程调用releaseShared()进行锁资源释放时,如果释放成功,则唤醒队列中等待的节点,如果有的话。

二、源码深入分析

基于上面所说的共享锁执行流程,我们接下来看下源码实现逻辑:

首先来看下获取锁的方法acquireShared(),如下

  1. public final void acquireShared(int arg) {
  2. //尝试获取共享锁,返回值小于0表示获取失败
  3. if (tryAcquireShared(arg) < 0)
  4. //执行获取锁失败以后的方法
  5. doAcquireShared(arg);
  6. }

这里tryAcquireShared()方法是留给用户去实现具体的获取锁逻辑的。关于该方法的实现有两点需要特别说明:

一、该方法必须自己检查当前上下文是否支持获取共享锁,如果支持再进行获取。

二、该方法返回值是个重点。其一、由上面的源码片段可以看出返回值小于0表示获取锁失败,需要进入等待队列。其二、如果返回值等于0表示当前线程获取共享锁成功,但它后续的线程是无法继续获取的,也就是不需要把它后面等待的节点唤醒。最后、如果返回值大于0,表示当前线程获取共享锁成功且它后续等待的节点也有可能继续获取共享锁成功,也就是说此时需要把后续节点唤醒让它们去尝试获取共享锁。

有了上面的约定,我们再来看下doAcquireShared方法的实现:

  1. //参数不多说,就是传给acquireShared()的参数
  2. private void doAcquireShared(int arg) {
  3. //添加等待节点的方法跟独占锁一样,唯一区别就是节点类型变为了共享型,不再赘述
  4. final Node node = addWaiter(Node.SHARED);
  5. boolean failed = true;
  6. try {
  7. boolean interrupted = false;
  8. for (;;) {
  9. final Node p = node.predecessor();
  10. //表示前面的节点已经获取到锁,自己会尝试获取锁
  11. if (p == head) {
  12. int r = tryAcquireShared(arg);
  13. //注意上面说的, 等于0表示不用唤醒后继节点,大于0需要
  14. if (r >= 0) {
  15. //这里是重点,获取到锁以后的唤醒操作,后面详细说
  16. setHeadAndPropagate(node, r);
  17. p.next = null;
  18. //如果是因为中断醒来则设置中断标记位
  19. if (interrupted)
  20. selfInterrupt();
  21. failed = false;
  22. return;
  23. }
  24. }
  25. //挂起逻辑跟独占锁一样,不再赘述
  26. if (shouldParkAfterFailedAcquire(p, node) &&
  27. parkAndCheckInterrupt())
  28. interrupted = true;
  29. }
  30. } finally {
  31. //获取失败的取消逻辑跟独占锁一样,不再赘述
  32. if (failed)
  33. cancelAcquire(node);
  34. }
  35. }

独占锁模式获取成功以后设置头结点然后返回中断状态,结束流程。而共享锁模式获取成功以后,调用了setHeadAndPropagate方法,从方法名就可以看出除了设置新的头结点以外还有一个传递动作,一起看下代码:

  1. //两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
  2. private void setHeadAndPropagate(Node node, int propagate) {
  3. Node h = head; //记录当前头节点
  4. //设置新的头节点,即把当前获取到锁的节点设置为头节点
  5. //注:这里是获取到锁之后的操作,不需要并发控制
  6. setHead(node);
  7. //这里意思有两种情况是需要执行唤醒操作
  8. //1.propagate > 0 表示调用方指明了后继节点需要被唤醒
  9. //2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
  10. if (propagate > 0 || h == null || h.waitStatus < 0 ||
  11. (h = head) == null || h.waitStatus < 0) {
  12. Node s = node.next;
  13. //如果当前节点的后继节点是共享类型获取没有后继节点,则进行唤醒
  14. //这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
  15. if (s == null || s.isShared())
  16. //后面详细说
  17. doReleaseShared();
  18. }
  19. }
  20. private void setHead(Node node) {
  21. head = node;
  22. node.thread = null;
  23. node.prev = null;
  24. }

最终的唤醒操作也很复杂,专门拿出来分析一下:

注:这个唤醒操作在releaseShare()方法里也会调用。

  1. private void doReleaseShared() {
  2. for (;;) {
  3. //唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
  4. //其实就是唤醒上面新获取到共享锁的节点的后继节点
  5. Node h = head;
  6. if (h != null && h != tail) {
  7. int ws = h.waitStatus;
  8. //表示后继节点需要被唤醒
  9. if (ws == Node.SIGNAL) {
  10. //这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark
  11. if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
  12. continue;
  13. //执行唤醒操作
  14. unparkSuccessor(h);
  15. }
  16. //如果后继节点暂时不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去
  17. else if (ws == 0 &&
  18. !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
  19. continue;
  20. }
  21. //如果头结点没有发生变化,表示设置完成,退出循环
  22. //如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
  23. if (h == head)
  24. break;
  25. }
  26. }

接下来看下释放共享锁的过程:

  1. public final boolean releaseShared(int arg) {
  2. //尝试释放共享锁
  3. if (tryReleaseShared(arg)) {
  4. //唤醒过程,详情见上面分析
  5. doReleaseShared();
  6. return true;
  7. }
  8. return false;
  9. }

注:上面的setHeadAndPropagate()方法表示等待队列中的线程成功获取到共享锁,这时候它需要唤醒它后面的共享节点(如果有),但是当通过releaseShared()方法去释放一个共享锁的时候,接下来等待独占锁跟共享锁的线程都可以被唤醒进行尝试获取。

三、总结

跟独占锁相比,共享锁的主要特征在于当一个在等待队列中的共享节点成功获取到锁以后(它获取到的是共享锁),既然是共享,那它必须要依次唤醒后面所有可以跟它一起共享当前锁资源的节点,毫无疑问,这些节点必须也是在等待共享锁(这是大前提,如果等待的是独占锁,那前面已经有一个共享节点获取锁了,它肯定是获取不到的)。当共享锁被释放的时候,可以用读写锁为例进行思考,当一个读锁被释放,此时不论是读锁还是写锁都是可以竞争资源的。

深入浅出AQS之共享锁模式的更多相关文章

  1. 深入浅出AQS之条件队列

    相比于独占锁跟共享锁,AbstractQueuedSynchronizer中的条件队列可能被关注的并不是很多,但它在阻塞队列的实现里起着至关重要的作用,同时如果想全面了解AQS,条件队列也是必须要学习 ...

  2. 深入浅出AQS之组件概览

    之前分析了AQS中的独占锁,共享锁,条件队列三大模块,现在从结构上来看看AQS各个组件的情况. 原文地址:http://www.jianshu.com/p/49b86f9cd7ab 深入浅出AQS之独 ...

  3. AQS 详解之共享锁模式

    概括 AQS框架数据结构是一个先进先出的双向队列,当多个线程进行竞争资源时,那些竞争失败的线程会加入到队列中.他向上层提供了很多接口,其中一个是acquireShared获取共享模式的接口.本文将会根 ...

  4. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...

  5. 并发编程-深入浅出AQS

    AQS是并发编程中非常重要的概念,它是juc包下的许多并发工具类,如CountdownLatch,CyclicBarrier,Semaphore 和锁, 如ReentrantLock, ReaderW ...

  6. canal源码之BooleanMutex(基于AQS中共享锁实现)

    在看canal源码时发现一个有趣的锁实现--BooleanMutex 这个锁在canal里面多处用到,相当于一个开关,比如系统初始化/授权控制,没权限时阻塞等待,有权限时所有线程都可以快速通过 先看它 ...

  7. 深入浅出AQS之独占锁模式

    每一个Java工程师应该都或多或少了解过AQS,我自己也是前前后后,反反复复研究了很久,看了忘,忘了再看,每次都有不一样的体会.这次趁着写博客,打算重新拿出来系统的研究下它的源码,总结成文章,便于以后 ...

  8. 从零开始了解多线程 之 深入浅出AQS -- 上

    java锁&AQS深入浅出学习--上 上一篇文章中我们一起学习了jvm缓存一致性.多线程间的原子性.有序性.指令重排的相关内容, 这一篇文章便开始和大家一起学习学习AQS(AbstractQu ...

  9. 深入浅出AQS源码解析

    最近一直在研究AQS的源码,希望可以更深刻的理解AQS的实现原理.虽然网上有很多关于AQS的源码分析,但是看完以后感觉还是一知半解.于是,我将自己的整个理解过程记录下来了,希望对大家有所帮助. 基本原 ...

随机推荐

  1. 【JBoss】数据库连接配置小结(转)

    数据库驱动位置: %JBOSS_HOME%\server\default\lib目录下. 数据库配置文件位置:JBOSS_HOME\docs\examples\jca\XXXX-ds.xml < ...

  2. python+selenium自动化软件测试(第16章):基础实战(3)

    #coding:utf-8 from time import sleep from selenium import webdriver class cloudedge_register(object) ...

  3. java se之File类

    遍历某个目录路径下的所有文件并打印输出: package com.led.file; import java.io.File; public class File_List { public stat ...

  4. python基础教程(二)

    继续第一篇的内容,讲解,python的一些基本的东西. 注释 为了让别人能够更容易理解程序,使用注释是非常有效的,即使是自己回头再看旧代码也是一样. >>> #获得用户名: > ...

  5. C# 反射、与dynamic最佳组合

    在 C# 中反射技术应用广泛,至于什么是反射.........你如果不了解的话,请看下段说明,否则请跳过下段.广告一下:希望我文章的朋友请关注一下我的blog,这也有助于提高本人写作的动力. 反射:当 ...

  6. oracle时间范围查询

    当时间精确到秒的指标和时间精确到日的时间进行对比是恒不等于的,但是可以判断大于或者等于的情况. 举个例子,指标[时间精确到秒] select 时间精确到秒 from table where 时间精确到 ...

  7. RPC框架实现思路浅析

    第一部分,设计分析 远程调用要解决的主要问题: 1,序列化 : 如何将对象转化为二进制数据进行传输,如何将二进制数据转化对象 2,数据的传输(协议,第三方框架) 3,服务的注册/发现,单点故障,分布式 ...

  8. 设置input的placeholder样式

    自定义input默认placeholder样式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 inpu ...

  9. indexOf和lastIndexOf方法

    lastIndexOf 方法: 返回 String 对象中子字符串最后出现的位置. strObj.lastIndexOf(substring[startindex]) 参数:strObj必选项.Str ...

  10. python-分页代码

    page.py ''' django内使用方式: all_count = models.UserInfo.objects.all().count() # path_info 当前页的url # all ...