java同步器AQS架构AbstractQueuedSynchronizer原理解析下

目录
  • 引导语
  • 1、释放锁
    • 1.1、释放排它锁release
    • 1.2、释放共享锁releaseShared
  • 2、条件队列的重要方法
    • 2.1、入队列等待await
      • 2.1.1、addConditionWaiter
      • 2.1.2、unlinkCancelledWaiters
    • 2.2、单个唤醒signal
    • 2.3、全部唤醒signalAll
  • 3、总结

引导语

AQS 的内容太多,所以我们分成了两个章节,没有看过 AQS 上半章节的同学可以回首看一下哈,上半章节里面说了很多锁的基本概念,基本属性,如何获得锁等等,本章我们主要聊下如何释放锁和同步队列两大部分。

1、释放锁

释放锁的触发时机就是我们常用的 Lock.unLock () 方法,目的就是让线程释放对资源的访问权(流程见整体架构图紫色路线)。

释放锁也是分为两类,一类是排它锁的释放,一类是共享锁的释放,我们分别来看下。

1.1、释放排它锁 release

排它锁的释放就比较简单了,从队头开始,找它的下一个节点,如果下一个节点是空的,就会从尾开始,一直找到状态不是取消的节点,然后释放该节点,源码如下:

// unlock 的基础方法
public final boolean release(int arg) {
    // tryRelease 交给实现类去实现,一般就是用当前同步器状态减去 arg,如果返回 true 说明成功释放锁。
    if (tryRelease(arg)) {
        Node h = head;
        // 头节点不为空,并且非初始化状态
        if (h != null && h.waitStatus != 0)
            // 从头开始唤醒等待锁的节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// 很有意思的方法,当线程释放锁成功后,从 node 开始唤醒同步队列中的节点
// 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
private void unparkSuccessor(Node node) {
    // node 节点是当前释放锁的节点,也是同步队列的头节点
    int ws = node.waitStatus;
    // 如果节点已经被取消了,把节点的状态置为初始化
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 拿出 node 节点的后面一个节点
    Node s = node.next;
    // s 为空,表示 node 的后一个节点为空
    // s.waitStatus 大于0,代表 s 节点已经被取消了
    // 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 这里从尾迭代,而不是从头开始迭代是有原因的。
        // 主要是因为节点被阻塞的时候,是在 acquireQueued 方法里面被阻塞的,唤醒时也一定会在 acquireQueued 方法里面被唤醒,唤醒之后的条件是,判断当前节点的前置节点是否是头节点,这里是判断当前节点的前置节点,所以这里必须使用从尾到头的迭代顺序才行,目的就是为了过滤掉无效的前置节点,不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞。
        for (Node t = tail; t != null && t != node; t = t.prev)
            // t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
            if (t.waitStatus <= 0)
                s = t;
    }
    // 唤醒以上代码找到的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

1.2、释放共享锁 releaseShared

释放共享锁的方法是 releaseShared,主要分成两步:

tryReleaseShared 尝试释放当前共享锁,失败返回 false,成功走 2;

唤醒当前节点的后续阻塞节点,这个方法我们之前看过了,线程在获得共享锁的时候,就会去唤醒其后面的节点,方法名称为:doReleaseShared。

我们一起来看下 releaseShared 的源码:

// 共享模式下,释放当前线程的共享锁
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        // 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法
        doReleaseShared();
        return true;
    }
    return false;
}

2、条件队列的重要方法

在看条件队列的方法之前,我们先得弄明白为什么有了同步队列,还需要条件队列?

主要是因为并不是所有场景一个同步队列就可以搞定的,在遇到锁 + 队列结合的场景时,就需要 Lock + Condition 配合才行,先使用 Lock 来决定哪些线程可以获得锁,哪些线程需要到同步队列里面排队阻塞;获得锁的多个线程在碰到队列满或者空的时候,可以使用 Condition 来管理这些线程,让这些线程阻塞等待,然后在合适的时机后,被正常唤醒。

同步队列 + 条件队列联手使用的场景,最多被使用到锁 + 队列的场景中。

所以说条件队列也是不可或缺的一环。

接下来我们来看一下条件队列一些比较重要的方法,以下方法都在 ConditionObject 内部类中。

2.1、入队列等待 await

// 线程入条件队列
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 加入到条件队列的队尾
    Node node = addConditionWaiter();
    // 标记位置 A
    // 加入条件队列后,会释放 lock 时申请的资源,唤醒同步队列队列头的节点
    // 自己马上就要阻塞了,必须马上释放之前 lock 的资源,不然自己不被唤醒的话,别的线程永远得不到该共享资源了
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 确认node不在同步队列上,再阻塞,如果 node 在同步队列上,是不能够上锁的
    // 目前想到的只有两种可能:
    // 1:node 刚被加入到条件队列中,立马就被其他线程 signal 转移到同步队列中去了
    // 2:线程之前在条件队列中沉睡,被唤醒后加入到同步队列中去
    while (!isOnSyncQueue(node)) {
        // this = AbstractQueuedSynchronizer$ConditionObject
        // 阻塞在条件队列上
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 标记位置 B
    // 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中的数据结构中去了
    // 所以这里节点苏醒了,直接尝试 acquireQueued
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        // 如果状态不是CONDITION,就会自动删除
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

await 方法有几点需要特别注意:

上述代码标记位置 A 处,节点在准备进入条件队列之前,一定会先释放当前持有的锁,不然自己进去条件队列了,其余的线程都无法获得锁了;上述代码标记位置 B 处,此时节点是被 Condition.signal 或者 signalAll 方法唤醒的,此时节点已经成功的被转移到同步队列中去了(整体架构图中蓝色流程),所以可以直接执行 acquireQueued 方法;Node 在条件队列中的命名,源码喜欢用 Waiter 来命名,所以我们在条件队列中看到 Waiter,其实就是 Node。

await 方法中有两个重要方法:addConditionWaiter 和 unlinkCancelledWaiters,我们一一看下。

2.1.1、addConditionWaiter

addConditionWaiter 方法主要是把节点放到条件队列中,方法源码如下:

// 增加新的 waiter 到队列中,返回新添加的 waiter
// 如果尾节点状态不是 CONDITION 状态,删除条件队列中所有状态不是 CONDITION 的节点
// 如果队列为空,新增节点作为队列头节点,否则追加到尾节点上
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    // 如果尾部的 waiter 不是 CONDITION 状态了,删除
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 新建条件队列 node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 队列是空的,直接放到队列头
    if (t == null)
        firstWaiter = node;
    // 队列不为空,直接到队列尾部
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

整体过程比较简单,就是追加到队列的尾部,其中有个重要方法叫做 unlinkCancelledWaiters,这个方法会删除掉条件队列中状态不是 CONDITION 的所有节点,我们来看下 unlinkCancelledWaiters 方法的源码,如下:

2.1.2、unlinkCancelledWaiters

// 会检查尾部的 waiter 是不是已经不是CONDITION状态了
// 如果不是,删除这些 waiter
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    // trail 表示上一个状态,这个字段作用非常大,可以把状态都是 CONDITION 的 node 串联起来,即使 node 之间有其他节点都可以
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        // 当前node的状态不是CONDITION,删除自己
        if (t.waitStatus != Node.CONDITION) {
            //删除当前node
            t.nextWaiter = null;
            // 如果 trail 是空的,咱们循环又是从头开始的,说明从头到当前节点的状态都不是 CONDITION
            // 都已经被删除了,所以移动队列头节点到当前节点的下一个节点
            if (trail == null)
                firstWaiter = next;
            // 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉,然后把自己挂到上一个状态是 CONDITION 的节点上
            else
                trail.nextWaiter = next;
            // 遍历结束,最后一次找到的CONDITION节点就是尾节点
            if (next == null)
                lastWaiter = trail;
        }
        // 状态是 CONDITION 的 Node
        else
            trail = t;
        // 继续循环,循环顺序从头到尾
        t = next;
    }
}

为了方便大家理解这个方法,画了一个释义图,如下:

2.2、单个唤醒 signal

signal 方法是唤醒的意思,比如之前队列满了,有了一些线程因为 take 操作而被阻塞进条件队列中,突然队列中的元素被线程 A 消费了,线程 A 就会调用 signal 方法,唤醒之前阻塞的线程,会从条件队列的头节点开始唤醒(流程见整体架构图中蓝色部分),源码如下:

// 唤醒阻塞在条件队列中的节点
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 从头节点开始唤醒
    Node first = firstWaiter;
    if (first != null)
        // doSignal 方法会把条件队列中的节点转移到同步队列中去
        doSignal(first);
}
// 把条件队列头节点转移到同步队列去
private void doSignal(Node first) {
    do {
        // nextWaiter为空,说明到队尾了
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // 从队列头部开始唤醒,所以直接把头节点.next 置为 null,这种操作其实就是把 node 从条件队列中移除了
        // 这里有个重要的点是,每次唤醒都是从队列头部开始唤醒,所以把 next 置为 null 没有关系,如果唤醒是从任意节点开始唤醒的话,就会有问题,容易造成链表的割裂
        first.nextWaiter = null;
        // transferForSignal 方法会把节点转移到同步队列中去
        // 通过 while 保证 transferForSignal 能成功
        // 等待队列的 node 不用管他,在 await 的时候,会自动清除状态不是 Condition 的节点(通过 unlinkCancelledWaiters 方法)
        // (first = firstWaiter) != null  = true 的话,表示还可以继续循环, = false 说明队列中的元素已经循环完了
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}   

我们来看下最关键的方法:transferForSignal。

// 返回 true 表示转移成功, false 失败
// 大概思路:
// 1. node 追加到同步队列的队尾
// 2. 将 node 的前一个节点状态置为 SIGNAL,成功直接返回,失败直接唤醒
// 可以看出来 node 的状态此时是 0 了
final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 当前队列加入到同步队列,返回的 p 是 node 在同步队列中的前一个节点
    // 看命名是 p,实际是 pre 单词的缩写
    Node p = enq(node);
    int ws = p.waitStatus;
    // 状态修改成 SIGNAL,如果成功直接返回
    // 把当前节点的前一个节点修改成 SIGNAL 的原因,是因为 SIGNAL 本身就表示当前节点后面的节点都是需要被唤醒的
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒
        LockSupport.unpark(node.thread);
    return true;
}

整个源码下来,我们可以看到,唤醒条件队列中的节点,实际上就是把条件队列中的节点转移到同步队列中,并把其前置节点状态置为 SIGNAL。

2.3、全部唤醒 signalAll

signalAll 的作用是唤醒条件队列中的全部节点,源码如下:

    public final void signalAll() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 拿到头节点
        Node first = firstWaiter;
        if (first != null)
            // 从头节点开始唤醒条件队列中所有的节点
            doSignalAll(first);
    }
    // 把条件队列所有节点依次转移到同步队列去
    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        do {
            // 拿出条件队列队列头节点的下一个节点
            Node next = first.nextWaiter;
            // 把头节点从条件队列中删除
            first.nextWaiter = null;
            // 头节点转移到同步队列中去
            transferForSignal(first);
            // 开始循环头节点的下一个节点
            first = next;
        } while (first != null);
    }

从源码中可以看出,其本质就是 for 循环调用 transferForSignal 方法,将条件队列中的节点循环转移到同步队列中去。

3、总结

AQS 源码终于说完了,你都懂了么,可以在默默回忆一下 AQS 架构图,看看这张图现在能不能看懂了。

以上就是java同步器AQS架构AbstractQueuedSynchronizer原理解析下的详细内容,更多关于java同步器AbstractQueuedSynchronizer的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java同步框架AbstractQueuedSynchronizer详解

    AbstractQueuedSynchronizer概述 AbstractQueuedSynchronizer是java中非常重要的一个框架类,它实现了最核心的多线程同步的语义,我们只要继承AbstractQueuedSynchronizer就可以非常方便的实现我们自己的线程同步器,java中的锁Lock就是基于AbstractQueuedSynchronizer来实现的.下面首先展示了AbstractQueuedSynchronizer类提供的一些方法: AbstractQueuedSynch

  • Java并发系列之AbstractQueuedSynchronizer源码分析(共享模式)

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快的理解其他的方式.虽然说AbstractQueuedSynchronizer源码有一千多行,但是重复的也比较多,所以读者不要刚开始的时候被吓到,只要耐着性子去看慢慢的自然能够渐渐领悟.就我个人经验来说,阅读AbstractQueuedSynchronizer源码有几个比较关键的地方需要弄明白,分别是

  • Java并发系列之AbstractQueuedSynchronizer源码分析(独占模式)

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概念,主要讲了AQS的排队区是怎样实现的,什么是独占模式和共享模式以及如何理解结点的等待状态.理解并掌握这些内容是后续阅读AQS源码的关键,所以建议读者先看完我的上一篇文章再回过头来看这篇就比较容易理解.在本篇中会介绍在独占模式下结点是怎样进入同步队列排队的,以及离开同步队列之前会进行哪些操作.AQS为在独占模

  • Java并发系列之AbstractQueuedSynchronizer源码分析(概要分析)

    学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore等.而这些类的底层实现都依赖于AbstractQueuedSynchronizer这个类,由此可见这个类的重要性.所以在Java并发系列文章中我首先对AbstractQueuedSynchronizer这个类进行分析,由于这个类比较重要,而且代码比较长,为了

  • Java 通过AQS实现数据组织

    引言 从本篇文章开始,我们将介绍 Java AQS 的实现方式,本文先介绍 AQS 的内部数据是如何组织的,后面的文章中再分别介绍 AQS 的各个部门实现. AQS 通过前面的介绍,大家一定看出来了,上述的各种类型的锁和一些线程控制接口(CountDownLatch 等),最终都是通过 AQS 来实现的,不同之处只在于 tryAcquire 等抽象函数如何实现.从这个角度来看,AQS(AbstractQueuedSynchronizer) 这个基类设计的真的很不错,能够包容各种同步控制方案,并提

  • java同步器AQS架构AbstractQueuedSynchronizer原理解析下

    目录 引导语 1.释放锁 1.1.释放排它锁release 1.2.释放共享锁releaseShared 2.条件队列的重要方法 2.1.入队列等待await 2.1.1.addConditionWaiter 2.1.2.unlinkCancelledWaiters 2.2.单个唤醒signal 2.3.全部唤醒signalAll 3.总结 引导语 AQS 的内容太多,所以我们分成了两个章节,没有看过 AQS 上半章节的同学可以回首看一下哈,上半章节里面说了很多锁的基本概念,基本属性,如何获得锁

  • java同步器AQS架构AbstractQueuedSynchronizer原理解析

    目录 引导语 1.整体架构 1.1.类注释 1.2.类定义 1.3.基本属性 1.3.1.简单属性 1.3.2.同步队列属性 1.3.3.条件队列的属性 1.3.4.Node 1.3.5.共享锁和排它锁的区别 1.4.Condition 2.同步器的状态 3.获取锁 3.1.acquire排它锁 3.1.1.addWaiter 3.1.2.acquireQueued 3.2.acquireShared获取共享锁 4.总结 引导语 AbstractQueuedSynchronizer 中文翻译叫做

  • Java流程控制顺序结构原理解析

    这篇文章主要介绍了Java流程控制顺序结构原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 流程控制的概念 在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的.也就是说,程序的流程对运行结果有直接的影响.所以,我们必须清楚每条语句的执行流程.而且,很多时候我们要通过控制语句的执行顺序来实现我们要完成的功能. 流程控制之顺序结构 根据代码的编写顺序,从上往下,依次执行. 顺序结构之流程图 ​ 需求 举例说明顺序结构的执行

  • Java方法覆盖重写实现原理解析

    这篇文章主要介绍了Java方法覆盖重写实现原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 方法覆盖重写注意事项: 1.必须保证方法名相同,返回值也相同 @Override:写在方法前面,用来检测方法的覆盖重写是否有效,这个注解不是必要的,就算不写,方法覆盖重写符合要求也是正确的 2.子类方法的返回值必须[小于等于]父类方法的返回值 3.子类方法的修饰符必须[大于等于]父类方法的修饰符 继承关系中,父子类构造方法的访问特点: 1.子类构造

  • java instanceof操作符使用及原理解析

    这篇文章主要介绍了java instanceof操作符使用及原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 a intanceof A:判断a是否是类A的的一个实例,返回值为boolean public class Person extends Object{} public class Student extends Person{} public class Graduate extends Person{} public clas

  • Java枚举类接口实例原理解析

    这篇文章主要介绍了Java枚举类接口实例原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 枚举类可以实现一个或多个接口.与普通类实现接口完全一样,枚举类实现接口时,需要实现该接口所包含的方法. 如果需要每个枚举值在调用同一个方法时呈现不同的行为,则可以让每个枚举值在{...}匿名块中实现自己的业务逻辑. public interface IGradeHandler { String getGrade(String studentName)

  • Java日期与时间类原理解析

    这篇文章主要介绍了Java日期与时间类原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 基础知识 日期: 类似于 2018-12-12 时间: 类似于 2018-12-12 12:12:12 时刻: 类似于 2018-12-12 12:12:12 地区: 计算机中的 Locale, 如 zh_CN, en_US 等, 影响着对于日期, 时间, 货币等格式的显示 EpochTime: 从 1970 年 1 月 1 日 UTC+00:00 到

  • Java三种移位运算符原理解析

    这篇文章主要介绍了Java三种移位运算符原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 java移位运算符不外乎就这三种:<<(左移).>>(带符号右移)和>>>(无符号右移). 1. 左移运算符 左移运算符<<使指定值的所有位都左移规定的次数. 1)它的通用格式如下所示: value << num num 指定要移位值value 移动的位数. 左移的规则只记住一点:丢弃最高位(符

  • Java基础异常处理代码及原理解析

    这篇文章主要介绍了java基础异常处理代码及原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 异常的定义:中断了正常指令流的事件. try..catch..finally结构: class Test{ public static void main(String[] args){ System.out.println(1); try{ System.out.println(2); int i = 1 / 0; System.out.pri

  • java对象序列化与反序列化原理解析

    这篇文章主要介绍了java对象序列化与反序列化原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.序列化和反序列化的概念 对象转换为字节序列的过程称为对象的序列化.把字节序列恢复为对象的过程称为对象的反序列化. 二.序列化和反序列化的作用 对象的序列化主要有两种用途: 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中. 在网络上传送对象的字节序列.网络上传输的都是二进制序列. 在很多应用中,需要对某些对象进行序列化,让它们离开内

随机推荐