• 中文
    • English
  • 注册
  • 查看作者
  • JAVA concurrency — AQS 源码详解

    概述

    全称 是jdk中一个非常重要的方法,这是一个jdk的同步器的实现,JUC中的很多类例如 等的实现都依赖于AQS。

    CAS

    AQS的同步实现方式依赖于CAS,那么CAS究竟是什么呢?

    全称 ,比较然后交换。JAVA中CAS的实现位于Unsafe类下。CAS本质上属于乐观锁,它的实现原理如下:当你想要修改位于某个内存地址 的值的时候,会带入两个值,一个是在地址上的旧值 ,一个是想要修改的新值 。比较内存地址上的值与 ,如果相同则将 的值更新入内存地址 中。

    有优点也有缺点,但是在本文中不详细阐述了,大家可以自行了解。在这里只是介绍下CAS是什么,为我们理解AQS的实现做好准备。

    AQS

    JAVA concurrency — AQS 源码详解

    这个是AQS内部维护的FIFO链表的示意图,我们可以看出每个节点会维护一个prev和一个next指针来维护双向链表。除此之外addWaiter额外维护了一个单向链表用于Condition的操作。每个Node节点内部会封装一个线程,当线程争抢锁失败后会封装成Node加入到ASQ队列中去。

    FIFO队列节点插入

    AQS内部维护了一个双向链表,链表的节点定义如下:

    链表插入的代码如下

    这里有几个需要注意的地方:

    1. 注意上述的代码 内部是一个无限循环,是为了要保证CAS操作一定要成功,如果不成功就反复尝试直到成功为止。

    2. 我们可以看到 方法中会有一次尝试直接把新节点放到尾部,这是一次尝试提高效率的操作。如果失败,再使用通用的 方法来加入节点。

    3. 当发现为节点为空的时候,不是用当前节点来初始化首尾,而是用一个空节点来作为虚拟头节点的存在。

    4. 此外上述插入新节点的代码里就利用到的CAS在内部进行了一次封装,具体的代码如下:

    AQS内部将CAS的代码再次进行了一层封装,使得它可以轻松调用于内部方法。

    AQS的共享模式与独占模式

    独占模式

    所谓独占模式,指的是同时只能有一个线程获取到同步器。例如可重入锁 就是一个AQS的独占模式的典型实现。

    AQS的独占模式有两个核心方法:

    1. 获取同步器 :

    获取同步器的方法比较简单,调用 来判断是否可以获取同步器,然后调用 来将新加入的节点放入队列。然后我们来看下这两个方法的具体实现,首先是 :

    我们可以看到 并没有在AQS内部实现,而是由AQS的具体实现类根据自己的需求自行实现的。那么再来看 :

    1. 释放同步器 :

    释放同步器的方法主要是这样的:首先调用 来看看是否满足释放同步器的条件,如果满足条件,那么需要在释放前先将后继节点唤醒(如果有后继节点,并且后继节点状态不为0)。来看下具体代码:

    可以看到和获取同步器一样 也是需要AQS实现类自己实现的。在唤醒后继节点时有这么一个问题,为什么需要从尾部开始遍历而不是从前面开始遍历?这里我们可以去看一下插入节点的代码,即 ,里面插入节点是在尾部插入的,代码是这样的:

    在CAS设置了尾节点的值之后,在 指向node之前,如果是从前开始遍历,遍历到这里就会发现节点为 ,这个时候就会漏掉部分节点。反之如果从后往前遍历则没有这些问题。

    共享模式

    所谓的共享模式,是指多个线程可以共享同一个同步器。

    共享模式的两个核心方法:

    1. 获取同步器 :

    和独占模式一样 同样需要子类自己实现。

    然后我们来看 :

    我们可以看到同步模式中和独占模式最大的不同是 ,我们看下具体实现:

    我们可以看到 中依然是调用了 方法,不同之处在于他会在设置完头节点后会根据条件释放后继节点。造成这点不同的原因就是因为在独占模式中,同时只能有一个线程占有同步器,所以在获取同步器的过程中不会出现需要唤醒其他线程的情况,但是在共享模式中,则可以有多个线程持有同步器。因此判断条件如下:

    1. : 当还剩有余量的时候

    2. : 当旧的头节点为空或者是状态为 或者 的时候

    3. : 当新的头节点为空或者是状态为 或者 的时候

    在这几种情况下,我们需要尝试着唤醒后面的节点来尝试获取同步器。至于唤醒方法,会在 部分解析。

    1. 释放同步器 :

    接下来看一下 和 里面都有调用了的 :

    其实这个方法不是很容易理解,这里进行下分解。首先我们观察可以注意到这是一个无限自旋的方法,唯一的一个跳出条件就是 ,也就是说,只有当h为头节点的时候才会跳出这个循环。然后我们来看下h的值是什么,我们可以看到h在循环的开始就被赋值为了头节点 这是怎么回事呢?这是因为在共享模式下不止一个线程可以获取到同步器,因此一个线程进行释放后续节点的操作时,其他节点可能也在进行这步操作,也就是说,在这个过程中头节点可能会进行变动。因此我们需要保证在每个线程内部如果头结点的值和自己预期不同就一直循环下去。

    然后我们来看这段代码:

    这段代码相对比较容易理解,如果一个节点的状态为 那么将它的值通过CAS,变为0,并且不断的失败重试直到成功为止。然后释放它的后继节点。

    比较令人费解的是下面这段代码:

    这段代码究竟是干什么的呢?我们来一步一步分析。首先ws什么时候会是0,那只有一种情况,那就是这个节点是新加入的节点,也就是说队列的最后的节点成为了队列的头节点。那么什么时候这个CAS会失败呢?只有当ws不为0的时候,也就是说只有在前一刻判断ws为0,下一刻ws被其他的线程修改导致不为0的时候才会走到这步 之中。至于为什么会有这一步操作呢?回想一下当ws为0的时候什么操作会改变ws的值。没错就是当有新的节点加入的时候,会调用到的 ,里面这段代码:

    在这种情况下确实是需要继续进行下一轮循环,然后唤醒后续的节点。确实是有道理,但是似乎优化的太细致了,不知道是不是我的理解不到位。

    Condition

    是jdk中定义的一个条件协作的接口,常用于阻塞队列等情况下。AQS内部有一个对其的实现。

    代码实现

    在AQS中定义了一个类 实现了 接口。

    在类中定义了两个Node,一个是 队列的头节点,一个是尾节点。还有一个比较重要的内部方法也放到这里讲: 。这个方法和之前的队列中的 有点像,但是区别在于他插入并不是依赖Node中的 和 ,而是 ,并且在代码中我们可以发现和之前的双向队列不同, 的队列是一个单向队列。

    中的主要方法有两个:

    1. :

    上面的代码加了一些注释,但是可能还是有点不清晰,所以逐步来进行讲解。首先这个 是什么东西呢?我们来看代码中的定义:

    表示该中断需要抛出异常, 则不同。那么再来看代码查询节点是否在队列中出现过是怎么实现的呢:

    然后我们来看下 这个条件在什么情况下成立,我们看下 的实现:

    这个方法实现很明了,如果一个线程被中断了,那么就根据 方法的结果来判断中断的类型,否则返回0。那么循环跳出的条件就很明了了,要么是节点已经在同步器队列内了,要么是线程被中断了(当然前提是有signal方法唤醒了阻塞的线程)

    1. :

    这里涉及到了两个方法:一个是 ,这个方法是由子类实现的,判断当前是否是在独占资源,另一个是 也就是 实现的核心方法,代码如下:

    当然 内部除了这两个核心方法之外还有诸如 , 等方法,实现大致相同,大家可以自行学习一下。

    总结

    java锁的基础实现靠的是AQS,AQS的基础使用的是CAS。AQS内部的实现依赖于FIFO双向队列,Condition的实现依靠的是一个单向链表。在AQS内部使用了大量的自旋操作,因而会对性能有一定的挑战,因此设计者在内部进行了大量的优化。在本文中未能将这些优化尽数到来,大家可以自己找一份源码细细品味。

  • 0
  • 0
  • 0
  • 13
  • 请登录之后再进行评论

    登录
  • 任务
  • 实时动态
  • 发布
  • 单栏布局 侧栏位置: