图解AbstractQueuedSynchronizer(AQS)实现原理:独占锁篇 笔记

介绍

AQS是JUC包中鼎鼎有名的并发工具基础类,许多并发工具如ReentrantLock、Semaphore、CountDownLatch、线程池的Worker都是以其为基础实现的。因此了解其运行原理十分有必要,本文为总结性的文章,不会对源码做过多分析,旨在描述清楚AQS的运作原理。详尽的源码分析可看这位大佬的文章一行一行源码分析清楚AQS

AQS是抽象队列同步器的简称。从他的名字我们可以了解到:

  • 抽象:只实现了一些主要逻辑,其余方法由子类实现
  • 队列:使用FIFO队列存储数据(线程节点)
  • 同步:实现了同步

AQS是一个可以用来构建锁和同步器的框架,使用他可以简单高效的构造出应用广泛的同步器,如上面提到的几个。

AQS提供了独占模式和共享模式的同步方式。本文分析AQS的独占锁篇,将以ReentrantLock为例。

AQS结构

基础字段

// 头结点,你直接把它当做 当前持有锁的线程 
private transient volatile Node head;

// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
private transient volatile Node tail;

// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
private volatile int state;

// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread;

接下来我们看AQS中的队列是什么样子的:
注意队列中不包含head节点!!。这张图很重要,请记住它!

可以看到阻塞队列是以双向链表的形式构建的,我们来看看Node的结构

static final class Node {
    // 标识节点当前在共享模式下
    static final Node SHARED = new Node();
    // 标识节点当前在独占模式下
    static final Node EXCLUSIVE = null;

    // ======== 下面的几个int常量是给waitStatus用的 ===========
    /** waitStatus value to indicate thread has cancelled */
    // 代码此线程取消了争抢这个锁
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    // 表示当前node的后继节点对应的线程需要被唤醒
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    // 表示当前节点在等待某一条件,条件成立才能被唤醒
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    // 共享模式下,表示还有资源可用,需要继续唤醒head的后继节点
    static final int PROPAGATE = -3;
    // =====================================================


    // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
    // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待
    volatile int waitStatus;
    // 前驱节点的引用
    volatile Node prev;
    // 后继节点的引用
    volatile Node next;
    // 这个就是节点对应的线程
    volatile Thread thread;
   	// Condition等待队列里下一个等待条件的结点
   	// 主要用在ReentrantLock的Condition中
    Node nextWaiter; 

}

注意:通过Node我们可以实现两个队列,一是通过prev和next实现CLH队列(线程同步队列,双向队列),二是nextWaiter实现Condition条件上的等待线程队列(单向队列),这个Condition主要用在ReentrantLock类中。

主要方法

AQS是基于模板方法模式设计的,也就是有一些方法必须由子类实现。主要有

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

也就是说,如果我们要构建自己的同步器,那么我们就要实现上面的方法,也可以理解为我们可以通过上面的方法定义自己的同步器规则。当然不用全部都实现,根据自己的需要选择独占方式或共享方式实现就行。

内部流程

接下来说说AQS为我们做了什么,他为我们实现了获取资源和释放资源的主要逻辑,也就是当抢不到资源时,何时需要挂起线程,何时需要唤醒线程。这些AQS帮我们实现了,我们只需要重写方法,定义自己的同步器规则就行。

我们以ReentrantLock的公平锁为例,我们知道ReentrantLock是可重入锁,那他是怎么实现的呢?ReentrantLock重写了tryAcquire和tryRelease,也就是可重入的实现规则是在这两个方法中定义的。
大家先看一下下面的流程图,蓝色方框的就是子类重写的方法,黑色方框的是AQS中的方法。
AQS的代码很巧妙,大家一定要仔细看。

看完流程,接下来总结下每个方法的流程:

  • acquire
    先通过tryAcquire尝试获取锁,成功了就直接返回;否则就调用acquireQueued将其加入到阻塞队列中,再判断是否需要将其挂起。
  • addWaiter(Node mode)
    将线程包装成Node节点,通过CAS插入到阻塞队列尾部。第一次CAS失败会调用enq(node)自旋CAS插入到队列尾部,此次一定保证插入成功。enq会初始化头结点。
  • acquireQueued( node ,1 )
    真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了。返回值是线程是否被中断过。
  • shouldParkAfterFailedAcquire
    判断线程是否需要挂起,如果前驱节点状态正常,则将其挂起;否则修正其前驱节点到正常的节点。
  • parkAndCheckInterrupt
    挂起线程并检查中断状态。
  • release
    释放锁资源。会先调用tryRelease,如果成功则唤醒head节点后继线程。

参考

第十一章 AQS
一行一行源码分析清楚AQS

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值