前言

适合读者:3 年以上经验的同学

谈到并发编程,基本上都会想到JDK 的 JUC 工具包,它包含 锁,并发工具类,原子类,线程池,还有阻塞队列,这是从网上找的一个大致的知识体系。

相信这些工具读者都见过并使用过一部分了,比如 CountDownLatch,线程池,原子类,但是可能不了解其中的原理,而面试可能要求更高一点,要求说出其原理,或者经常有这么一问,如果是你,你会怎么去实现。

本文主要讲 AQS 的实现,需要你有如下基础

  • 队列 使用双向链表实现,添加节点和删除节点的操作
  • 在多线程并发情况下,双向链表的添加或删除节点会有线程安全问题,导致死循环或节点为空,要求能想到这种场景
  • 设计模式,主要是模板方法模式,这个简单
  • volatile 关键字的理解,主要是变量在主存和线程副本之间立即可见,它并不能保证原子性,如果想深究,需要看到 JVM 内存模型
  • 线程的六种状态,主要是要区别 waiting 和 timed-waiting , sleep 方法和 wait 和 Condition 方法的 await 都是使线程进入 timed-waiting ,UNSAFE.park(thread) 是调用本地方法,不需要获取锁即可使线程进入 waiting 状态
  • CAS 全称 compareAndSwap ,它的参数一般是 (对象,比较的属性,预期值,目标值),意思是对这个对象的这个属性做变更操作,如果预期值和内存值一致,则将内存值修改为目标值,它是一条 cpu 原语,能保证原子性
  • 中断 (interrupt) 的意思,它只会中断阻塞,即退出阻塞状态,并不会中断线程的运行,可能我们经常的被它抛出的 InterruptException 给误解了。
  • 最好有使用过 CountDownLatch ReentrantLock 或 AtomicXX 之类的类

主要内容

本文参考自 https://www.cnblogs.com/waterystone/p/4920797.html 个人不太喜欢贴源码的风格,把一个很简单的东西说得很复杂了。

AQS是AbstractQueuedSynchronizer的简称,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。

就是多个线程对共享资源 state 的获取与释放,AQS 在顶层维护了一个线程队列,它实现了线程的出队,入队,唤醒,和节点状态的维护,使用模板方法模式,为子类提供了一些有用的方法,子类只要实现资源的获取与释放即可。

它有两对方法分别用于独占资源和共享资源 acquire-release,acquireShared-releaseShared

下面 -- 标记的都是方法名,可以自己去源码中查看,如果当前方法不清楚含义可以来这里看

下面的流程一定要对照源码 AbstractQueuedSynchronizer 来查看,不然可能不知道我在说什么,这里说的是独占模式和共享模式资源的获取和释放,这个框架只会来管你有几个资源,不会管你资源的其它属性。

获取独占资源的流程是这样子的 -- acquire

  1. 先尝试获取资源,如果获取到资源了,直接返回 -- tryAcquire
  2. 如果未获取到资源,则创建一个节点添加到队列尾 -- addWaiter
  3. 以自旋的方式判断是否排队排到了第一个,但虽然你排到了第一个,也要由子类的 tryAcquire 来决定你能不能获取资源 -- acquireQueued
  4. 如果不能获取资源,则先让前面的弄完了通知我一下 -- shouldParkAfterFailedAcquire,我先休息下(这时候线程阻塞处于 waiting 状态) -- parkAndCheckInterrupt

释放独占资源的流程是这样子的 -- release

  1. 先尝试释放资源 -- tryRelease
  2. 如果释放成功,从队列后面往前找,找到队列最前面那个正在等待的家伙,叫醒它; 这里如果认真看了一定会有一个疑问,为什么要从队列后面找,从前面找不是更快吗,文章末尾给出答案 ,这里你理解流程和使用 -- unparkSuccessor

tryAcquireShared 的含义是,如果返回的结果小于 0 ,表示没有可用资源,如果等于 0 表示最后一个可用资源,如果大于 0 ,表示获取成功并且还有可用资源

获取共享资源流程是这样子的 -- acquireShared

  1. 先尝试获取共享资源,如果成功直接返回 -- tryAcquireShared
  2. 如果获取失败,以共享模式添加到队尾 -- addWaiter
  3. 以自旋的方式判断是否排队排到了第一个,但虽然你排到了第一个,也要由子类的 tryAcquireShared 来决定你能不能获取资源 -- doAcquireShared
  4. 如果获取到资源了,但是资源还有剩余,叫醒后面的弟兄 -- setHeadAndPropagate
  5. 如果不能获取资源,则先让前面的弄完了通知我一下 -- shouldParkAfterFailedAcquire,我先休息下(这时候线程阻塞处于 waiting 状态) -- parkAndCheckInterrupt

释放共享资源流程

  1. 先尝试释放资源 -- tryReleaseShared
  2. 如果释放成功,从队列后面往前找,找到队列最前面那个正在等待的家伙,叫醒它 --doReleaseShared

节点状态 waitStatus

​ 这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

注意:负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

在独占模式中只需要关注 3 个状态 > 0 的为取消,0 是默认就是这个状态 < 0 是可唤醒后继节点

学习这个不能只看这个队列,因为它是抽象出来的一个框架,需要结合具体的使用示例,如 CountDownLatch ,去看它是如何工作的。

CountDownLatch 构造了一个有 n 个资源的同步队列,使用共享模式,因为会有多个线程同时访问资源,每当 countDown 的时候,去释放了一个资源,然后在主线程 await 的时候去获取一个共享资源,但这个获取资源是只有资源量为 0 的时候才是成功的,核心代码就一句话。

protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}

再看看独占锁的使用 ReentrantLock ,它使用独占资源,同一时刻只能有一个线程访问共享资源,增加了一个属性 OwnerThread ,当再次是同一个线程获取锁的时候是可重入的,有公平锁和非公平锁的实现,非公平锁在进行 lock 的时候 ,会先去试图将 state 设置为 1 ,预期为 0 ,如果成功,则成功插队,公平锁在 tryAcquire 增加了一个判断 hasQueuedPredecessors 如果有线程在排队就乖乖排队吧,并且没有在 lock 的时候尝试去占用资源 。

这里解释下在释放资源的时候 unparkSuccessor 方法为什么要从队尾开始找,从前面开始找不是更快吗

要理解这个,你首先得理解双向队列是如何插入节点的,假设有这样一个队列

head = A <---> B = tail

需要往队列尾加入节点 C ,应该是这样操作的

C.prev = tail;
tail.next = C;
tail = C;

在多线程并发情况下,这里多步操作并不是原子性的,tail 属于临界资源随时可能被其它线程修改成其它指向。

假设这种场景 C 在入队的时候 ,进行 tail.next =C 这一步前 ,D 也在入队,如果 C 先完成,这时队列结构会变成这样子

head = A <---> B <---> C

​ B <---> D = tail

AQS 使用了自旋和 CAS 来解决这个麻烦 ,它的代码是这样子的,在 enq 函数中

 for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}

这个函数的主要作用是当那[一瞬间]在队列为空的时候进入这个方法创建队列,有可能在正想初始化队列的时候已经被其它线程初始化了,所以初始化时使用了 CAS compareAndSetHead(),然后再把 tail 的指向和 head 的指向一致,这里因为另一个线程不能初始化成功,所以这里来说是正确的。

再看 else ,当其它线程已经把队列初始化完成 ,tail 不为空的时候会进入这里,这个 t 只是表示这一瞬间的尾结点,尾节点可能被其它线程给修改了,使用 CAS compareAndSetTail 保证了只有当尾节点是这个瞬时尾结点的时候才能修改 node.prev=t ,保证了,从尾节点是一定可以找到前置节点,但设置完尾节点,然后才把那个尾节点的后置节点指向 node ,不一定能保证根据 tail (在遍历的时候不一定是尾节点了) 一定能找到 next 。

head = A <---> B <--- C = tail C 还没走 t.next = node 这一步,这时 D CAS 失败,D 认为的 tail 为 B

​ B <--- D

从此图可以看出,最终于,从后往前找链表是连续的,但从前往后找是不一定的,回答了最开始的问题。

写得有点晦涩,我也是想以最通俗易懂的方式写出来,但表达水平有限,望大神指正。

一点小推广

创作不易,希望可以支持下我的开源软件,及我的小工具,欢迎来 gitee 点星,fork ,提 bug 。

Excel 通用导入导出,支持 Excel 公式

博客地址:https://blog.csdn.net/sanri1993/article/details/100601578

gitee:https://gitee.com/sanri/sanri-excel-poi

使用模板代码 ,从数据库生成代码 ,及一些项目中经常可以用到的小工具

博客地址:https://blog.csdn.net/sanri1993/article/details/98664034

gitee:https://gitee.com/sanri/sanri-tools-maven

Java AQS 的胡言乱语修正版的更多相关文章

  1. 一个使用MVC3+NHibernate “增删改查” 的项目(修正版)

      前言: 谈到NHibernate大伙并不陌生,搞Java的更是清楚,Hibernate是一个目前应用的最广泛的开放源代码的对象关系映射框架,它对Java的JDBC(类似于ADO.Net)进行了非常 ...

  2. Win7全自动精简批处理_温柔处理极速修正版/暴力剩女工程测试版

    2011htpcfans 发表于 2012-5-11 http://bbs.wuyou.net/forum.php?mod=viewthread&tid=210269&highligh ...

  3. 如何把程序钉到Windows7任务栏(修正版)

    源:如何把程序钉到Windows7任务栏(修正版) 在CSDN论坛看到有网友提问如何把程序钉到Windows7的任务栏,ccrun(妖哥)对这个问题很感兴趣,于是google了一下,没有找到相关的AP ...

  4. java AQS(AbstractQueuedSynchronizer)同步器详解

    除了内置锁(synchronized)外,java AQS(AbstractQueuedSynchronizer)同步器几乎是所有同步容器,同步工具类的基础.ReentrantLock.Reentra ...

  5. JAVA AQS源码分析

    转自:  http://www.cnblogs.com/pfan8/p/5010526.html JAVA AQS的全称为(AbstractQueuedSynchronizer),用于JAVA多线程的 ...

  6. RT-thread v2.1.0修正版

    RT-Thread v2.1.0是v2.0.1正式版这个系列的bug修正版.RT-Thread v2.1.0修正的主要内容包括: 这个版本经历的时间比较长,并且原定的一些目标也还未能完成(更全的POS ...

  7. Java AQS学习

    参考原文: Java并发之AQS详解 <Java并发编程的艺术> AQS 概述 AQS简介 AQS(AbstractQueuedSynchronizer)就是一个抽象的队列同步器,它是用来 ...

  8. 基于BranchTraceStore机制的CPU执行分支追踪工具 —— CpuWhere [修正版 仅驱动]

    [前言] 在张银奎老师的<软件调试>一书中,详细地讲解了使用内存的分支记录机制——BTS机制(5.3),并且给出了示例工具CpuWhere及其源代码.但实际运行(VMware XP_SP3 ...

  9. 精美图文讲解Java AQS 共享式获取同步状态以及Semaphore的应用

    | 好看请赞,养成习惯 你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it ...

随机推荐

  1. jquery+layer实现无刷新、删除功能(laravel框架)

    先来看一下效果 路由代码 Route::get('car/{id}/delete', 'CarController@delete'); 控制器层代码 //删除汽车信息 public function ...

  2. JS 获取随机颜色值

    获取随机颜色值 function fn1(){ return '#' + Math.floor( Math.random() * 0xffffff ).toString(16); } function ...

  3. 随笔js

    js中的函数定义之后,函数名就是这个函数(JS中函数其实也是对象)的地址(句柄) js读取函数内存地址: 首先想读内存地址只有C或者C++,汇编抛开不谈,其他高级语言一般都封装起来了,不过我也不能确定 ...

  4. NOIP2016普及组解题报告

    概述 \(NOIP2016\)普及组的前三题都比较简单,第四题也有很多的暴力分,相信参加了的各位\(OIer\)在\(2016\)年都取得了很好的成绩. 那么,我将会分析\(NOIP2016\)普及组 ...

  5. Centos7 安装 Cockpit

    1sudo yum -y install epel-release sudo yum -y update sudo shutdown -r now 2yum -y install cockpit sy ...

  6. GitHub项目简介

    为了存放代码新建了一个GitHub账号,存放了一些比较常用的代码块,上面的模块大部分都能找到 index.html 文件直接在浏览器打开. 地址:https://github.com/liuzhou1 ...

  7. xshell 链接虚拟机

    1.在虚拟机上添加网络适配器,选择仅主机模式 2. 2.启用本机的网络连接 3.在虚拟机上开启ssh服务 首先看下22端口有没开放,如果没有的话需要开启 service start sshd 4.在虚 ...

  8. Saber-图集

    PS:狙击手

  9. java.sql.SQLException: Access denied for user 'gaoqi'@'127.0.0.1' (using password: YES)

    GRANT ALL PRIVILEGES ON *.* TO root @'%' IDENTIFIED BY "mypassword";--%表示所有的IP都能访问,也可以修改为专 ...

  10. bugku 宽带信息泄露

    首先下载文件 下载完成后发现是一个后缀名为 bin 的文件 然后找百度查一下这是什么文件的后缀名 看一下题目 然后用软件routerpassview打开(搜的教程) 然后打开文件 搜索username ...