认真的并发基石:AQS源码分析与图解


theme: juejin

highlight: androidstudio

学习并发,理解并发,掌握并发是Java程序员迈不过去的一道坎。现实业务中很多情况下都会涉及到并发操作,知己知彼百战不殆。学好并发知识非常且极其有必要。这篇文章我们就来分析下并发知识中 非常核心 的一个知识点:AbstractQueuedSynchronizer 简称(AQS)。

在开篇前,我们先看下本文的结构: image.png

说明: 1. 本文jdk版本为:11 1. 为了方便下面我们提到AbstractQueuedSynchronizer类时 一般都用AQS简称。 2. AQS类本质上是模板方法模式,所以在学习AQS时,最好要知道什么是模板方法模式知道其结构。 3. 由于AQS内部使用state来表示锁资源,所以本文提到资源时 也大多使用state表示。 4. 在学习AQS之前,希望对锁知识以及Java中的并发工具有所了解和使用,这样可以达到事半功倍的效果。 5. 在AQS中有挺多CAS操作,希望你对CAS有所了解知道他是怎么做的以及使用场景。 6. 由于源码分析比较枯燥,所以本文在每一个源码小节后都会配一张图解(因为一图胜千言,我个人比较喜欢画图来解释一些比较重要或者难懂的知识点) 7. AQS是JDK层面的锁实现,如果想了解jvm层面的锁实现则移步我的另一篇文章:万字长文分析synchroized:,两者对比下,你会发现似乎有些地方是同样的设计逻辑 8. 本文依赖jdk11源码中有部分和VarHandle相关(比如CLH的入队操作),不了解的最好去看一下VarHandle是个啥。

1、AQS概述

所谓AQS(AbstractQueuedSynchronizer)中文直译抽象队列同步器,他定义了一套多线程访问共享资源的同步器框架,提供了SDK层面的锁机制,很多类都是基于这个大拿 开发的比如: ReentrantLock/Semaphore/CountDownLatch/ReentrantReadWriteLock/ThreadPoolExecutor中的Worker/以及jdk之外的很多开源项目......等都是基于它。我们简单看下我本地的AQS使用情况:image.png

通过查阅作者的对于该类的文档注释可以得到如下核心信息: image.png

我们来大体概括下上图这段英文信息,就能对AQS有一个基础的认识了。如下:

  1. AQS用一个 volatile int state; 属性表示锁状态(因为锁是存在并发获取的,所以必须要被可见的,即保证a修改后b立即在主内存可见!),1表示锁被持有,0表示未被持有AQS类提供了修改该属性的三个方法: getState() , setState(int newState) , compareAndSetState(int expect, int update) 。
  2. 框架内部维护了一个FIFO的等待队列,是用双向链表实现的,我们称之为CLH队列。
  3. 框架内部也实现了条件变量 Condition ,用它来实现等待唤醒机制,并且支持多个条件变量(本文我们不做分析留到下篇文章)。
  4. AQS支持两种资源共享的模式: 独占模式(Exclusive)和共享模式(Share),所谓独占模式就是任 意时刻只允许一个线程访问共享资源,譬如ReentrantLock;而共享模式指的就是允许多个线程同时访 问共享资源,譬如Semaphore/CountDownLatch
  5. 使用者只需继承 AQS 并重写指定的方法,在方法内完成对共享资源 state 的获取和释放,至于具体线程等待队列的维护,AQS已经在顶层实现好了,在那些模板 方法里。

2、AQS原理与结构

2.1、AQS原理简介

宏观上看,AQS其实就是俩东西组成: 一个是资源 state,一个是未获取到资源的线程的 等待队列 CLH

什么是CLH? : CLH锁其实就是当多个线程竞争同一把锁时,获取不到锁的线程,会排队进入CLH队列(FIFO)的队尾,然后自旋等待,直到其前驱线程释放锁。由于是 Craig、Landin 和 Hagersten三位大佬的发明,因此命名为CLH锁。

CLH队列中存放的是?: 存放的是一个个的Node对象(而Node中存放的东西我们后边再说,总之我们知道Node中一定会有对应的线程信息)

我们先看下AQS大体的原理图(更多细节的东西在下边会讲到)。 image.png

我们简单举个例子,在排他锁模式下流程如下: 1. 假设t1时刻,有线程a持有资源state(持有资源的线程一定是在head节点这个我们一定要清楚) 2. t1时刻,线程b试图调用获取锁的方法来获取锁资源,发现获取锁失败,则将线程b的相关数据封装为Node并插入CLH队列的队尾。 3. 挂起线程b,并告知线程a(通过将head节点的waitStatus设置为SIGNAL),资源释放了记得通知我啊! 4. t2时刻,线程a释放资源(并将对应Node赋值为null,利于GC)state后通知线程b 5. t3时刻 线程b 尝试获取锁(此时如果是公平锁则大概率可以获取成功,如果是非公平,则不一定)

以上这个只是个大概的流程,期间有很多优化和细节操作。后边源码我们逐一分析在这里我们只需要知道他的主题逻辑就行了。

2.2、AQS结构认识

在分析AQS源码前,我们首先要和AQS内部的兄弟 混个脸熟,所以有了本小节。

首先我们看下 AQS 的 继承关系图,如下: image.png 可以看到 我们常见的并发工具(ReentrantLock/Semaphore/CountDownLatch/ThreadPoolExecutor/ReentrantReadWriteLock),都是直接或间接的继承自AQS类。其实到最后我们会发现,搞懂了AQS这个知识点,上边括号中的那几个并发工具类 原理也就豁然开朗了。

AQS中相当重要的三个成员变量(头/尾节点+state): ```java //头节点(独占锁模式下,持有资源的永远都是头节点!这个要知道哦) private transient volatile Node head; //尾节点 private transient volatile Node tail; //锁资源(无锁状态是0,每次加锁成功后,通过cas进行+1,在重入场景下,重入几次就是几) private volatile int state;

```

AQS中的两个内部类:ConditionObjectNode image.png

Node类:

下边是Node类的源码,先简单看一下: ```java static final class Node { //当前节点处于共享模式的标记 static final Node SHARED = new Node();

//当前节点处于独占模式的标记
    static final Node EXCLUSIVE = null;

    //线程被取消
    static final int CANCELLED =  1;
    //head持有锁线程释放资源后需唤醒后继节点
    static final int SIGNAL    = -1;
    //等待condition唤醒
    static final int CONDITION = -2;
    //工作于共享锁状态,需要向后传播,
    static final int PROPAGATE = -3;

    //等待状态,有1,0,-1,-2,-3五个值。分别对应上面的值
    volatile int waitStatus;

    //前驱节点
    volatile Node prev;

    //后继节点
    volatile Node next;

    //等待锁的线程
    volatile Thread thread;

    //等待条件的下一个节点,ConditonObject中用到
    Node nextWaiter;

} ```

因为

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值