当前位置:

首页 > 编程开发 > java同步器AQS架构如何释放锁和同步队列

java同步器AQS架构如何释放锁和同步队列

引导语AQS的内容太多,所以我们分成了两个章节,没有看过AQS上半章节的同学可以回首看一下哈,上半章节里面说了很多锁的基本概念,基本属性,如何获得锁等等,本章我们主要聊下如何释放锁和同步队列两大部分。1、释放锁释放锁的触发时机就是我们常用的Lock.unLock()方法,目的就是让线程释放对资源的访问权(流程见整体架构图紫色路线)。释放锁也是分为两类,一类是排它锁的释放,一类是共享锁的释放,我们分别来看下。1.1、释放排它锁release排它锁的释放就比较简单了,从队头开始,找它的下一个节点,如果下一个节

本文目录

      引导语

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

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

      java同步器AQS架构如何释放锁和同步队列

      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架构如何释放锁和同步队列

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java
      相关文章 更多
      Java单元测试入门:使用JUnit 5编写第一个测试用例
      Java单元测试入门:使用JUnit 5编写第一个测试用例

      面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

      Java变量未初始化报错解析:局部变量与成员变量默认值差异
      Java变量未初始化报错解析:局部变量与成员变量默认值差异

      深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

      详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

      Java中final与static final常量的定义及初始化规则
      Java中final与static final常量的定义及初始化规则

      深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

      Java抽象方法与接口方法的区别及选型建议
      Java抽象方法与接口方法的区别及选型建议

      深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

      Java实现HTTP请求与JSON解析的完整代码示例
      Java实现HTTP请求与JSON解析的完整代码示例

      本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

      Java递归出现StackOverflowError的原因分析及栈空间优化方案
      Java递归出现StackOverflowError的原因分析及栈空间优化方案

      详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

      Java空指针异常(NPE)的代码排查与修复实例
      Java空指针异常(NPE)的代码排查与修复实例

      针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

      selenium上传文件教程本地文件路径设置方法
      selenium上传文件教程本地文件路径设置方法

      深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

      针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

      查看更多
      精品专题 更多
      装机必备
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows
      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。