概括

AQS框架数据结构是一个先进先出的双向队列,当多个线程进行竞争资源时,那些竞争失败的线程会加入到队列中。他向上层提供了很多接口,其中一个是acquireShared获取共享模式的接口。本文将会根据这个接口一步步分析,获取资源失败的线程是怎么进入到队列中的,进入到队列中又是怎么出队列再次竞争资源的,下面是acquireShared执行的一个大致流程:

  1. 多个线程通过调用tryAcquireShared方法获取共享资源,返回值大于等于0则获取资源成功,返回值小于0则获取失败。

  2. 当前线程获取共享资源失败后,通过调用addWaiter方法把该线程封装为Node节点,并设置该节点为共享模式。然后把该节点添加到队列的尾部。

  3. 添加到尾部后,判断该节点的上一个节点是不是队列的头节点,如果是头节点,那么该节点的上一个节点出队列并获取共享资源,同时调用setHeadAndPropagate方法把该节点设置为新的头节点,同时唤醒队列中所有共享类型的节点,去获取共享资源。如果获取失败,则再次加入到队列中。

  4. 如果该节点的前驱节点不是头节点,那么通过for循环进行自旋转等待,直到当前节点的前驱节点是头节点,结束自旋。

这就是AQS共享模式竞争资源失败的大致流程,这里先让大家有一个大致的印象,下面通过源码具体分析是怎么进行操作的。

AQS共享锁模式

AQS获取共享锁是通过调用acquireShared() 这个顶层方法,我们看一下这个方法的源代码:

public final void acquireShared(int arg) {
   if (tryAcquireShared(arg) < 0)
       doAcquireShared(arg);
}

这个方法中有一个if判断,当tryAcquireShared()这个返回值是小于0的时候获取锁失败,进入doAcquireShared()方法。tryAcquireShared方法是用来获取共享模式下的锁,对于tryAcquireShared()这个方法我们重点看一下他的返回值。jdk1.8中是这样写的

* @return a negative value on failure; zero if acquisition in shared
*         mode succeeded but no subsequent shared-mode acquire can
*         succeed; and a positive value if acquisition in shared
*         mode succeeded and subsequent shared-mode acquires might
*         also succeed, in which case a subsequent waiting thread
*         must check availability. (Support for three different
*         return values enables this method to be used in contexts
*         where acquires only sometimes act exclusively.)  Upon
*         success, this object has been acquired.

当失败的时候返回的是负值,如果返回的是0表示获取共享模式成功但是它下一个节点的共享模式无法获取成功。如果返回的是正数也就是大于0,表示当前线程获取共享模式成功,并且它后面的线程也可以获取共享模式。

当共享模式获取失败的时候,我们看一下doAcquireShared源代码做了哪些操作

private void doAcquireShared(int arg) {
   final Node node = addWaiter(Node.SHARED);
   boolean failed = true;
   try {
       boolean interrupted = false;
       for (;;) {
           final Node p = node.predecessor();
           if (p == head) {
               int r = tryAcquireShared(arg);
               if (r >= 0) {
                   setHeadAndPropagate(node, r);
                   p.next = null; // help GC
                   if (interrupted)
                       selfInterrupt();
                   failed = false;
                   return;
              }
          }
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())
               interrupted = true;
      }
  } finally {
       if (failed)
           cancelAcquire(node);
  }
}

首先调用addWaiter()方法,它主要是封装为Node节点,并且把该节点添加到队列的尾部。此处传入共享模式的参数,节点就变成了共享模式。

当前线程添加到队列后,然后通过自旋(for(;;))获取前驱节点,如果前驱节点是头节点,那么调用tryAcquireShared()方法获取当前节点的状态,注意此方法的返回值在上面已经介绍过,等于0表示不用唤醒后继节点,只有大于0才会唤醒后面的所有节点。

如果获取共享资源成功,调用setHeadAndPropagate方法设置当前节点为头节点,并让原来的头节点出队列。如果在获取锁自旋的过程中中断过,那么将当前线程中断。

如果当前节点的前驱节点不是头节点,通过shouldParkAfterFailedAcquire判断当前线程的状态,如果线程阻塞返回true,否则返回false. parkAndCheckInterrupt方法是指当前线程在获取锁的过程中是否被中断唤醒,如果当前线程状态阻塞并且被中断过那么就把标志为interrupted更新为true。

如果发生异常调用cancelAcquire方法,此方法是把当前节点先更新为取消状态,并清除该节点。

setHeadAndPropagate我们看一下这个方法的源代码

private void setHeadAndPropagate(Node node, int propagate) {
   Node h = head; // Record old head for check below
   setHead(node);//设置当前节点为头节点
   if (propagate > 0 || h == null || h.waitStatus < 0 ||
      (h = head) == null || h.waitStatus < 0) {//符合状态的将全部唤醒
       Node s = node.next;
       if (s == null || s.isShared())
           doReleaseShared();
  }
}

此方法传递了2个参数,一个是当前节点,一个是tryAcquireShared方法的返回值。从源代码中我们看到它首先记录了当前头节点,然后它通过setHead()方法把当前获取到锁的节点设置为头节点。通过if语句把符合条件的继续唤醒后继节点,如果下一个节点为空那么调用doReleaseShared方法,doReleaseShared方法继续唤醒后面的节点。此方法会在共享锁释放详细讲解。

共享锁释放

我们来看一下releaseShared的源代码,此方法是共享模式释放资源的顶层方法。

public final boolean releaseShared(int arg) {
   if (tryReleaseShared(arg)) {//
       doReleaseShared();
       return true;
  }
   return false;
}

tryReleaseShared方法获取共享模式资源释放,如果释放成功那么会调用doReleaseShared继续唤醒下一个节点.

我们继续看一下具体的唤醒操作doReleaseShared() 这个方法

private void doReleaseShared() {
   for (;;) {
       Node h = head;
       if (h != null && h != tail) {
           int ws = h.waitStatus;
           if (ws == Node.SIGNAL) {
               if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                   continue;            // loop to recheck cases
               unparkSuccessor(h);
          }
           else if (ws == 0 &&
                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
               continue;                // loop on failed CAS
      }
       if (h == head)                   // loop if head changed
           break;
  }
}

通过源代码我们发现,当前线程状态如果是Node.SIGNAL,Node.SIGNAL的值是-1,是一个静态常量,此值表示当前线程被挂起。如果当前线程被挂起,那么更新当前线程的状态值为0.如果更新失败那么就继续。更新成功后调用unparkSuccessor()此方法是唤醒共享锁的第一个节点。如果本身头节点属于重置状态waitStatus==0,并且把它设置为传播状态那么就向下一个节点传播。

我们再看一下unparkSuccessor这个方法的源码

private void unparkSuccessor(Node node) {

   int ws = node.waitStatus;
   if (ws < 0)
       compareAndSetWaitStatus(node, ws, 0);

   Node s = node.next;
   if (s == null || s.waitStatus > 0) {
       s = null;
       for (Node t = tail; t != null && t != node; t = t.prev)
           if (t.waitStatus <= 0)
               s = t;
  }
   if (s != null)
       LockSupport.unpark(s.thread);
}

从这个方法中我们发现如果当先线程的状态是小于0,那么就把当前线程重置为0.为什么是小于0呢,上篇文章已经讲过,waitStatus<0为等待或挂起状态。也就是如果当前线程是等待挂起状态,那么把当前线程状态重置为0。然后找到下一个节点,如果下一个节点是空或下一个线程已经被取消,那么就从头部找下一个没有被取消的节点。当下一个节点不为空的时候,调用LockSupport.unpark方法唤醒当前线程。LockSupport.unpark会调用Unsafe这个类调用native方法进行执行。

AQS 详解之共享锁模式的更多相关文章

  1. (转)Java并发包基石-AQS详解

    背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方. Java并发包基石-AQS详解Java并发包( ...

  2. javascript设计模式详解之命令模式

    每种设计模式的出现都是为了弥补语言在某方面的不足,解决特定环境下的问题.思想是相通的.只不过不同的设计语言有其特定的实现.对javascript这种动态语言来说,弱类型的特性,与生俱来的多态性,导致某 ...

  3. javascript设计模式详解之策略模式

    接上篇命令模式来继续看下js设计模式中另一种常用的模式,策略模式.策略模式也是js开发中常用的一种实例,不要被这么略显深邃的名字给迷惑了.接下来我们慢慢看一下. 一.基本概念与使用场景: 基本概念:定 ...

  4. 图解SynchronousQueue原理详解-非公平模式

    SynchronousQueue原理详解-非公平模式 开篇 说明:本文分析采用的是jdk1.8 约定:下面内容中Ref-xxx代表的是引用地址,引用对应的节点 前面已经讲解了公平模式的内容,今天来讲解 ...

  5. 详解Mac睡眠模式设置

    详解Mac睡眠模式设置 原文链接:http://www.insanelymac.com/forum/index.php?showtopic=281945 需要说明的是,首先这篇文章是针对已经能够成功睡 ...

  6. AQS详解之独占锁模式

    AQS介绍 AbstractQueuedSynchronizer简称AQS,即队列同步器.它是JUC包下面的核心组件,它的主要使用方式是继承,子类通过继承AQS,并实现它的抽象方法来管理同步状态,它分 ...

  7. AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  8. Java并发之AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  9. 【1】AQS详解

    概述: 它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点加入到同步队列尾部(采用自旋CAS来 ...

随机推荐

  1. MSTP多生成树协议

    MSTP多生成树协议 目录 MSTP多生成树协议 1.MSTP(Multiple Spanning Tree Protocol)概述 2.STP.RSTP.PVST的应用缺陷 3.MSTP的主要特点 ...

  2. 微服务从代码到k8s部署应有尽有系列(一)

    从本篇文章开始,我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 实战项目地址:https://github.com/Mikaelemmmm/go-zer ...

  3. C++ 反汇编:关于函数调用约定

    函数是任何一门高级语言中必须要存在的,使用函数式编程可以让程序可读性更高,充分发挥了模块化设计思想的精髓,今天我将带大家一起来探索函数的实现机理,探索编译器到底是如何对函数这个关键字进行实现的,并使用 ...

  4. Solution Set -「ARC 107」

    「ARC 107A」Simple Math   Link.   答案为: \[\frac{a(a+1)\cdot b(b+1)\cdot c(c+1)}{8} \] 「ARC 107B」Quadrup ...

  5. Eureka工作原理及心跳机制

    Eureka原理 1.基本原理上图是来自eureka的官方架构图,这是基于集群配置的eureka:处于不同节点的eureka通过Replicate进行数据同步Application Service为服 ...

  6. 自动同步bing壁纸

    在百度搜东西,经常出来一大坨广告:要么就是复制粘贴文章.完全没有创新,搜索越来越困难.偶尔用一下bing还挺好用. bing的壁纸是真心好看,每天不重样.决定写个脚本同步一下它的壁纸. 一.以我的Wi ...

  7. VS Code 在线运行:code-server部署(系列一)

    偶然间发现VS Code可以在线运行,闲来无事部署了一套试试效果,先上图,是不是很熟悉的感觉,初步体验基本跟本地运行效果一样. 进入正题 首先要准备一套linux环境,环境配置最低是1G内存.2核CP ...

  8. 零基础入门Python游戏学习笔记(1)

    书是车洪于2020年出的,到手已经过去一年多了.现在学来,好多东西不一样了. 作者的GitHub,大家知道的原因,并不好打开. 代码就不搬了,只是为了学习方便,书籍勘误搬一下. 一.开发环境: 1.p ...

  9. RFC2544学习频率“Learning Frequency”详解—信而泰网络测试仪实操

    在RFC2544中, 会有一个Learning Frequency的字段让我们选择, 其值有4个, 分别是learn once, learn Every Trial, Learn Every Fram ...

  10. 3款著名BI工具功能盘点,哪款更实用?

    随着信息技术的发展和应用, 人类进入了一个大数据时代.5G的到来也在不断影响我们的生活,很明显,5G将在世界上留下持久的印记和影响力.每秒10千兆比特的传输速度,它比4G网络的连接速度提高了100倍左 ...