前言

由于AQS的源码太过凝练,而且有很多分支比如取消排队、等待条件等,如果把所有的分支在一篇文章的写完可能会看懵,所以这篇文章主要是从正常流程先走一遍,重点不在取消排队等分支,之后会专门写一篇取消排队和等待条件的分支逻辑。读源码千万别在每个代码分支中来回游走,先按一个正常的分支把流程看明白,之后再去重点关注其他分支,各个击破。我相信看完正常流程,你再去分析其他分支会更加得心应手。本篇将主要方法名都做了目录索引,查看时可通过目录快速跳到指定方法的逻辑。

执行流程

AQS的执行流程大体为当线程获取锁失败时,会加入到等待队列中,在等待队列中的线程会按照从头至尾的顺序依次再去尝试获取锁执行。

当线程获取锁后如果还需要等待特定的条件才能执行,那么线程就加入到条件队列排队,当等待的条件到来时再从条件队列中按照从头至尾的顺序加入到等待队列中,然后再按照等待队列的执行流程去获取锁。所以AQS最核心的数据结构其实就两个队列,等待队列和条件队列,然后再加上一个获取锁的同步状态。

AQS数据结构

AQS最核心的数据结构就三个

  • 等待队列

    源码中head和tail为等待队列的头尾节点,在通过前后指向则构成了等待队列,为双向链表,学名为CLH队列。

  • 条件队列

    ConditionObject中的firstWaiter和lastWaiter为等待队列的头尾节点,然后通过next指向构成了条件队列,是个单向链表。

  • 同步状态

    state为同步状态,通过CAS操作来实现获取锁的操作。

public abstract class AbstractQueuedSynchronizer{

  /**
* 等待队列的头节点
*/
private transient volatile Node head; /**
* 等待队列的尾节点
*/
private transient volatile Node tail; /**
* 同步状态
*/
private volatile int state; public class ConditionObject implements Condition, java.io.Serializable { /** 条件队列的头节点 */
private transient Node firstWaiter; /** 条件队列的尾节点 */
private transient Node lastWaiter;
}
}

Node节点

两个队列中的节点都是通过AQS中内部类Node来实现的。主要字段:

  • waitStatus

    当前节点的状态,具体看源码列出的注释。很重要,之后会在源码中讲解。

  • Node prev

    等待队列节点指向的前置节点

  • Node next

    待队列节点指向的后置节点

  • Node nextWaiter

    条件队列中节点指向的后置节点

  • Thread thread

    当前节点持有的线程

static final class Node {
/** */
static final Node SHARED = new Node();
/** */
static final Node EXCLUSIVE = null; /** 标明当前节点线程取消排队 */
static final int CANCELLED = 1; /** 标明该节点的后置节点需要自己去唤醒 */
static final int SIGNAL = -1; /** 标明当前节点在等待某个条件,此时节点在条件队列中 */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3; /**
* 等待状态,值对于上面的四个常量
*/
volatile int waitStatus; /**
* 等待队列节点指向的前置节点
*/
volatile Node prev; /**
* 等待队列节点指向的后置节点
*/
volatile Node next; /**
* 当前节点持有的线程
*/
volatile Thread thread; /**
* 条件队列中节点指向的后置节点
*/
Node nextWaiter;

加锁

上面说明的数据结构我们先大致有个印象,现在通过加锁来一步步说明下具体的流程,上篇文章JUC并发编程基石AQS之结构篇,我们知道了AQS加锁代码执行的是acquire方法,那么我们从这个方法说起,从源码中看出执行流程为:tryAcquire——>addWaiter——>acquireQueued

tryAcquire为自己实现的具体加锁逻辑,当加锁失败时返回false,则会执行addWaiter,将线程加入到等待队列中,Node.EXCLUSIVE为独占锁的模式,即同时只能有一个线程获取锁去执行。

例子说明

首先假设有四个线程t0-t4调用tryAcquire获取锁,t0线程为天选之子获取到了锁,则t1-t4线程接着去执行addWaiter。

acquire

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

addWaiter分支1

addWaiter方法,首先会初始化一个node节点,将当前线程设置到node节点中。然后判断head和tail节点是否为空,head和tail节点是懒加载的,当AQS初始化时为null,则第一次进来时if (pred != null) 条件不成立,执行enq方法。

例子说明

假如t1和t2线程同时执行到该方法,head节点未初始化则执行enq。

private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

enq

此时可能多个线程会同时调用enq方法,所以该方法中也使用CAS操作。for (;;)是个死循环,首先会CAS操作初始化head节点,且head节点是个空节点,没有设置线程。然后第二次循环时通过CAS操作将该节点设置我尾部节点,并将前置节点指向head,之后会跳出循环,返回生成的Node节点到addWaiter,从源码可以看到addWaiter方法后面没有逻辑,之后会调用acquireQueued。

例子说明

t1和t2线程同时执行,t1线程上天眷顾CAS成功,则流程为

  • 初始化head

  • t1线程的node节点加入等待队列

  • t2线程执行,node节点加入等待队列

private Node enq(final Node node) {
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;
}
}
}
}

addWaiter分支2

现在在来说t3和t4,t3和t4线程这时终于获取到了cpu的执行权,此时head节点已经初始化,则进入条件中的代码,其实也是通过CAS操作将节点加入到等待队列尾部,之后会调用acquireQueued。

例子说明

假如t3线程先CAS成功,之后t4成功,此时的数据结构为

private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

acquireQueued

这个方法有两个逻辑,首先如果该节点的前置节点是head会走第一个if,再次去尝试获取锁???

获取锁成功,则将头节点设置为自己,并返回到acquire方法,此时acquire方法执行完,代表获取锁成功,线程可以执行自己的逻辑了。这里有下面几个注意点

  • p.next = null; // help GC 设置旧的head节点的后置节点为null
  • setHead方法 将t1节点设置为头节点,因为头节点是个空节点,所以设置t1线程节点线程为null,设置t1前置节点为null,此时旧的head节点已经没有任何指向和关联,可以被gc回收,所以上面那一步会写个help GC 的注释。

例子说明

现在t1线程的前置节点为头结点,如果t1执行tryAcquire成功则结果为

当获取锁失败或者前置节点不是头节点都会走第二个if逻辑,首先会判断当前线程是否需要挂起,如果需要则执行线程挂起。

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
} private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}

shouldParkAfterFailedAcquire

判断线程是否需要挂起,首先需要注意的是这个方法的参数是当前节点的前置节点。当线程需要挂起的时候,它需要把身后事安排明白,挂起后让谁来把我唤醒。这个方法就主要做这个操作。我们再来看Node节点中的waitStatus状态,这个状态有一个Node.SIGNAL=-1,代表了当前节点需要将后置节点唤醒。这个理解可能有点绕。首先我们要理解一点,如果我需要被唤醒,那么我就要设置我们的前置节点的状态为Node.SIGNAL,这样当我的前置节点发现waitStatus=Node.SIGNAL时,它才知道,我执行完后需要去唤醒后置节点让后置节点去执行。所以这个方法是当前节点去设置自己的前置节点的状态为Node.SIGNAL

waitStatus初始化后是0,

第一次进入该方法,发现自己的前置节点不是Node.SIGNAL,需要先设置为Node.SIGNAL状态

第二次进入时发现前置节点已经是Node.SIGNAL状态,那么我就可以安心的挂起了,有人会唤醒我的。

所以这个方法其实是两个逻辑,先设置前置节点状态,再判断是否可以挂起。因为前面acquireQueued方法中for (;

JUC并发编程基石AQS之主流程源码解析的更多相关文章

  1. JUC并发编程基石AQS源码之结构篇

    前言 AQS(AbstractQueuedSynchronizer)算是JUC包中最重要的一个类了,如果你想了解JUC提供的并发编程工具类的代码逻辑,这个类绝对是你绕不过的.我相信如果你是第一次看AQ ...

  2. JUC 并发编程--11, AQS源码原理解析, ReentrantLock 源码解读

    这里引用别人博客,不重复造轮子 https://blog.csdn.net/u012881584/article/details/105886486 https://www.cnblogs.com/w ...

  3. 【Java并发编程】22、Exchanger源码解析(JDK1.7)

    Exchanger是双向的数据传输,2个线程在一个同步点,交换数据.先到的线程会等待第二个线程执行exchangeSynchronousQueue,是2个线程之间单向的数据传输,一个put,一个tak ...

  4. JUC并发编程学习笔记

    JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...

  5. Scala 深入浅出实战经典 第67讲:Scala并发编程匿名Actor、消息传递、偏函数解析

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...

  6. Spring IOC容器启动流程源码解析(四)——初始化单实例bean阶段

    目录 1. 引言 2. 初始化bean的入口 3 尝试从当前容器及其父容器的缓存中获取bean 3.1 获取真正的beanName 3.2 尝试从当前容器的缓存中获取bean 3.3 从父容器中查找b ...

  7. Hadoop中Yarnrunner里面submit Job以及AM生成 至Job处理过程源码解析

    参考 http://blog.csdn.net/caodaoxi/article/details/12970993 Hadoop中Yarnrunner里面submit Job以及AM生成 至Job处理 ...

  8. 并发编程-深入浅出AQS

    AQS是并发编程中非常重要的概念,它是juc包下的许多并发工具类,如CountdownLatch,CyclicBarrier,Semaphore 和锁, 如ReentrantLock, ReaderW ...

  9. JUC : 并发编程工具类的使用

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.JUC是什么 1.JUC定义 JUC,即java.util.concurrent 在并发编程中使用的 ...

随机推荐

  1. C++最简打开网页的方法

    system("explorer https://pay.747fz.com");

  2. iOS 图片加载和处理

    一.图片显示 图片的显示分为三步:加载.解码.渲染.解码和渲染是由 UIKit 进行,通常我们操作的只有加载. 以 UIImageView 为例.当其显示在屏幕上时,需要 UIImage 作为数据源. ...

  3. Redis启动出现creating server tcp listening socket错误

    错误如图所示 解决方法 在命令行中运行 redis-cli.exe 127.0.0.1:6379>shutdown not connected>exit 然后重新运行redis-serve ...

  4. 玩转redis-简单消息队列

    使用go语言基于redis写了一个简单的消息队列 源码地址 使用demo redis的 list 非常的灵活,可以从左边或者右边添加元素,当然也以从任意一头读取数据 添加数据和获取数据的操作也是非常简 ...

  5. 马哥教育PYTHON相关基础 笔记

    1 python 推荐书籍 <python Cookbook> <learn python the hard way> <google's python class> ...

  6. 谷歌 MapReduce 初探

    谷歌“三驾马车”的出现,才真正把我们带入了大数据时代,毕竟没有谷歌,就没有大数据. 上次的分享,我们对谷歌的其中一驾宝车 GFS 进行了管中窥豹,虽然只见得其中一斑,但是也能清楚的知道 GFS 能够把 ...

  7. 透过源码分析ArrayList运作原理

    List接口的主要实现类ArrayList,是线程不安全的,执行效率高:底层基于Object[] elementData 实现,是一个动态数组,它的容量能动态增加和减少.可以通过元素下标访问对象,使用 ...

  8. php--phpstudy更新数据库版本后,无法一键启动

    只需输入以下命令即可: sc delete mysql

  9. 关于Git我们不得不知道的事(一)

    一.什么是Git? Git是目前世界上最先进的分布式版本控制系统(没有之一). Git可以协助我们很方便的管理我们的项目,我们随时可以找回(或者回到)我们之前任何一个时刻的项目:还可以让同事或者开发小 ...

  10. Vulnhub webdeveloper靶机渗透

    信息搜集 nmap -sP 192.168.146.0/24 #主机发现 nmap -A 192.168.146.148 #综合扫描 访问一下发现是wordpress,wp直接上wpscan wpsc ...