队列同步器(AbstractQueuedSynchronizer)源码简析

本文介绍了Java并发编程中的AbstractQueuedSynchronizer(AQS)核心组件,它是构建锁和其他同步组件的基础。AQS通过一个int同步状态变量管理线程竞争,使用FIFO队列来处理等待线程。文章详细分析了AQS的常用方法,包括独占式和共享式获取与释放同步状态,以及线程的阻塞和唤醒机制。通过对AQS的理解,可以更好地掌握ReentrantLock、Semaphore等同步器的实现原理。
摘要由CSDN通过智能技术生成

文章后续于https://github.com/zgkaii/CS-Study-Notes更新,欢迎批评指正!

1 AQS概述

AbstractQueuedSynchronizer,即队列同步器,一般简称AQS。它是构建锁或者其他同步组件的基础(如 Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock),是 JUC 并发包中的核心基础组件

抽象了竞争的资源和线程队列,使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如上篇文章写的ReentrantLock与ReentrantReadWriteLock。除此之外,AQS还能构造出Semaphore,FutureTask(jdk1.7) 等同步器。

AQS 的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。

AQS 使用一个 int 类型的成员变量 state表示同步状态

  • state > 0 时,表示已经获取了锁。
  • state = 0 时,表示释放了锁。

它提供了三个方法,来对同步状态 state 进行操作,并且 AQS 可以确保对 state 的操作是安全的:

  • #getState()
  • #setState(int newState)
  • #compareAndSetState(int expect, int update)

可以把AQS理解为抽象队列式的同步器,它有两种资源共享方式:独占|共享,子类负责实现公平|非公平,下面我们会详细讲到。


如何理解AQS与锁的关系呢?

其实很好理解,锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

2 AQS常用方法

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

private volatile int state;// 共享变量,使用volatile修饰保证线程可见性

同步状态state通过 protected 类型的getStatesetStatecompareAndSetState方法进行操作

// 返回同步状态的当前值
protected final int getState() {
   
        return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
   
        state = newState;
}
// CAS更新同步状态,该方法能够保证状态设置的原子性
protected final boolean compareAndSetState(int expect, int update) {
   
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

自定义同步器时需要重写下面几个 AQS 提供的模板方法:

tryAcquire(int arg)		// 独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获						   // 取同步状态。成功则返回true,失败则返回false。
tryRelease(int arg)		// 独占式释放同步状态。成功则返回true,失败则返回false。
tryAcquireShared(int arg)// 共享式获取同步状态,返回值大于等于0,则表示获取成功;否则,获取失败。
tryReleaseShared(int arg)// 共享式释放同步状态。成功则返回true,失败则返回false。
isHeldExclusively()		 // 当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占。
    					 // 只有用到condition才需要去实现它。

AQS 还主要提供了如下方法

  • acquire(int arg):独占式获取同步状态。如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步队列等待。该方法将会调用可重写#tryAcquire(int arg) 方法;
  • #acquireInterruptibly(int arg):与 #acquire(int arg) 相同,但是该方法响应中断。当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException 异常并返回。
  • #tryAcquireNanos(int arg, long nanos):超时获取同步状态。如果当前线程在 nanos 时间内没有获取到同步状态,那么将会返回 false ,已经获取则返回 true 。
  • #acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
  • #acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断。
  • #tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制。
  • #release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。
  • #releaseShared(int arg):共享式释放同步状态。

从上面的方法看下来,同步器提供的模板方法基本上分为3类:

  • 独占式获取与释放同步状态
  • 共享式获取与释放同步状态
  • 查询同步队列中的等待线程情况。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。

以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。

再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后续动作。

但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

2 AQS实现分析

2.1 同步队列

AQS 是依赖 CLH 队列锁来完成同步状态的管理。如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程;当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(FIFO双向队列)(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配

同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点信息。

属性类型与名称 描述
int waitStatus 等待状态(如CANCELLED=1、SIGNAL=-1、CONDITION=-2、PROPAGATE=-3、INITIAL=0)
Node prev 前驱节点(当节点加入同步队列时被设置,在尾部添加)
Node next 后继节点
Thread thread 当前获取同步状态的线程

节点源码如下:

static final class Node {
   
    // 表示该节点等待模式为共享式,通常记录于nextWaiter,
    // 通过判断nextWaiter的值可以判断当前结点是否处于共享模式
    static final Node SHARED = new Node();
    // 表示节点处于独占式模式,与SHARED相对
    static final Node EXCLUSIVE = null;
    // waitStatus的不同状态
    // 当前结点是因为超时或者中断取消的,进入该状态后将无法恢复
    static final int CANCELLED =  1;
    // 当前结点的后继结点是(或者将要)由park导致阻塞的,当结点被释放或者取消时,需要通过unpark唤醒后继结点
    static final int SIGNAL    = -1;
    // 表明结点在等待队列中,结点线程等待在Condition上
    // 当其他线程对Condition调用了signal()方法时,会将其加入到同步队列中   
    static final int CONDITION = -2;
    // 下一次共享式同步状态的获取将会无条件地向后继结点传播
    static final int PROPAGATE = -3;
    volatile int waitStatus;
    // 记录前驱结点
    volatile Node prev;
    // 记录后继结点
    volatile Node next;
    // 记录当前的线程
    volatile Thread thread;
    // 用于记录共享模式(SHARED), 也可以用来记录CONDITION队列
    Node nextWaiter;
    // 通过nextWaiter的记录值判断当前结点的模式是否为共享模式
    final boolean isShared() {
   	return nextWaiter == SHARED;}
    // 获取当前结点的前置结点
    final Node predecessor() throws NullPointerException {
    ... }
    // 用于初始化时创建head结点或者创建SHARED结点
    Node() {
   }
    // 在addWaiter方法中使用,用于创建一个新的结点
    Node(Thread thread, Node mode) {
        
        this.nextWaiter = mode;
        this.thread = thread;
    }
	// 在CONDITION队列中使用该构造函数新建结点
    Node(Thread thread, int waitStatus) {
    
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}
// 记录头结点
private transient volatile Node head;
// 记录尾结点
private transient volatile Node tail;

2.1.1 入队

节点是构成同步队列的基础,同步器拥有首节点(Head)和尾节点(Tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect, Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。关联过程:(1)tail 指向新节点。(2)新节点的 prev 指向当前最后的节点。(3)当前最后一个节点的 next 指向当前节点。

入队逻辑是实现的 #addWaiter(Node) 方法,需要考虑并发的情况。它通过 CAS 的方式,来保证正确的添加 Node 。

    private Node addWaiter(Node mode) {
   
        // 新建节点。在创建的构造方法,`mode` 方法参数,传递获取同步状态的模式。
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值