AbstractQueuedSynchronizer原理及应用

163 阅读9分钟

前言

AbstractQueuedSynchronizer简称(AQS),它位于java.util.concurrent.locks,它是一个提供了同步状态、阻塞和唤醒线程以及队列模型用于实现锁的模板类。本篇文章会先介绍AQS的一些基本方法,然后再通过ReentrantLock公平锁的源码,来解读AQS在独占锁的相关知识。

官方文档

Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.

它提供了一个基于FIFO等待队列来实现阻塞锁或者相关同步器的框架

Subclasses should be defined as non-public internal helper classes that are used to implement the synchronization properties of their enclosing class. Class AbstractQueuedSynchronizer does not implement any synchronization interface.

子类应该定义一个非public的用于实现同步属性的帮助类,AQS自身并没有任何同步接口

AQS详解

AbstractQueuedSynchronizer是一个抽象类,它继承了AbstractOwnableSynchronizer,AbstractOwnableSynchronizer也是一个抽象方法,在这个类的内部只有同步器持有的线程(exclusiveOwenerThread)以及对应的getter,setter方法。

image-20230225074624524

AbstractQueuedSynchronizer整体结构如下图所示,包括了同步器状态(state)和是在独占模式下同步器持有的线程(exclusiveOwenerThread)以及CLH队列。

CLH队列

Craig, Landin, and Hagersten是三位计算机科学家Craig,Landin以及Hagersten的名字首字母命名的一个FIFO双向队列。

image-20230225081051004

CLH队列中的每个Node节点保存等待线程以及等待状态,进入等待对接的node节点状态为0,是初始状态,其他节点状态见代码中的常量

// 取消状态,由于在同步对接中等待的线程等待超市或者被中断,需要从同步队列中取消等待
static final int CANCELLED =  1;
// 后继节点线程处于等待状态,而当前节点的线程如果释放了同步状态或被取消,将会通知后继节点,使得后继节点得以运行
static final int SIGNAL    = -1;
// 节点在等待队列中,节点线程等待在Condition上,其他线程对condition调用了signal()方法后
static final int CONDITION = -2;
// 共享同步状态获取将会无条件地被传播下去
static final int PROPAGATE = -3;

AQS中提供的模板方法

AQS提供了如下模板方法

  • final void acquire(int arg)

    • 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法
  • final void acquireInterruptibly(int arg)

    • acquire(int arg)的基础上增加了响应中断,如果当前线程被中断,则该方法会抛出InterruptedException
  • final boolean tryAcquireNanos(int arg, long nanosTimeout)

    • 在acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了返回true
  • final void acquireShared(int arg)

    • 共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,它与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态
  • final void acquireInterruptibly(int arg)

    • acquireShared(int arg)基础上增加了相应中断
  • final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

    • acquireInterruptibly(int arg)基础上增加了超时限制
  • final boolean release(int arg)

    • 释放独占试同步状态
  • final boolean releaseShared(int arg)

    • 释放共享试同步状态

AQS中可重写的方法

AQS虽然是一个抽象类,但整个类中都没有抽象方法,AQS中提供了下面5个可重写的方法,它默认都抛出了UnsupportedOperationException,Doug Lea大佬没有把这几个方法定义为抽象类,主要是因为AQS太强大了,只需要根据需求重写相应的方法,不用重写所有方法

// 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
// 独占式释放同步状态
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
// 共享试获取同步状态
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
// 共享试释放同步状态
protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException();  }
// 当前同步器是否在独占模式下被线程占用,一般用于表示是否被当前线程所独占
protected boolean isHeldExclusively() { throw new UnsupportedOperationException(); }

Java中基于AQS的实现类

在JUC包中,有5个类基于AQS实现锁,实现方式与官方文档相同,都在类中包含一个非public的Sync类。其中ReentrantLock,Semaphore,ReentrantReadWriteLock实现了公平锁和非公平锁。

公平锁与非公平锁

  • 公平锁: 多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁
  • 非公平锁: 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

image-20230225110606385

以ReentrantLock为例分析ReentrantLock和AQS源码

我们以相对更常用的ReentrantLock为例分析AQS中公平锁加锁和解锁的过程。

ReentrantLock加锁

ReentrantLock加锁过程如下,关键方法是AbstractQueueSynchronizer#acquire,整个步骤大致分为两

  1. 首先判断CLH队列中是否有正在等待的线程,如果没有则尝试获取锁
  2. 如果获取锁失败,创建当前线程的node节点进入CLH队列中排队

image-20230225122712976

我们假设已经有线程获得锁了,模拟线程进入CLH排队的过程,在方法进入AQS的acquire,首先会调用ReentrantLock.FairSync实现的方法

// AQS#acquire
public final void acquire(int arg) {
    // 调用ReentrantLock.FairSync的tryAcquire方法尝试获取锁
    if (!tryAcquire(arg) &&
        // 如果上面获取不到锁,则生成Node节点并排队
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

首先调用tryAcquire尝试获取锁,FairSync逻辑如下

  • 返回true: 获取到锁

    • state=0,没有线程持有锁,并且CLH队列没有等待线程,当前线程CAS修改state成功
    • state!=0,同步器是由当前线程持有(锁重入)
  • 返回false: 获取锁失败

    • state=0,没有线程持有锁,或CLH队列有等待线程,或者当前线程CAS修改state失败
    • state!=0,同步器不是由当前线程持有

在ReentrantLock中,公平同步器(FairSync)与非公平同步器(NonfairSync)在实现tryAcquire的唯一区别是state=0时,非公平锁不会判断CLH队列中是否有等待的线程,直接进行CAS更新state操作。也就是说当state=0时,CLH中有等待线程的情况下,后来的线程也有可能插队获取锁

// ReentrantLock.FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // C==0说明目前没有线程持有锁
    if (c == 0) {
        // 因为是公平锁所以要先判断CLH队列中是否有线程排队
        if (!hasQueuedPredecessors() &&
            // 如果没有线程排队,并且使用CAS修改state状态成功,则返回true
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // C不等于0并且当前锁节点,则更新state的值+1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

如果线程抢到锁,AQS#acquire直接返回,如果该线程没抢到锁,则会调用AQS#addWaiter方法,如果CAS更新tail节点成功,则直接返回node,否则进入enq入队方法,以下情况会进入enq方法

  • 如果tail节点为空,CLH初始化后没有线程入队,此时head节点和tail节点都是空,队列中也没有虚拟节点(dummy node)
  • CAS 更新tail节点失败

虚拟节点(dummy node)

虚拟节点是对链表操作的常用技巧,它的 next 指针指向链表的头节点。这样一来,我们就不需要对头节点进行特殊的判断了,可省去许多麻烦。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {     // 如果tail节点!=null,则进行CAS操作,
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {  // CAS set tail节点操作
            pred.next = node;
            return node;
        }
    }
    enq(node); // 入队操作
    return node;
}

enq方法其实就是一个死循环,如果没有dummy node,则会创建一个dummy node,否则不断地CAS将当前节点加入到队尾

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

acquireQueued方法内部是一个死循环,如果当前节点是队列的head节点,则会尝试获取锁,否则判断是否应该被阻塞,如果被应该被阻塞,则底层调用LockSupport阻塞当前线程。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 如果前驱节点是head节点,head节点是虚拟节点,则说明当前节点在CLH队列的队首,可以尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 判断单线node节点是否应该阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

判断当前节点是否应该被阻塞,如果前驱节点的waitStatus为Node.SIGNAL(-1),则说明前面节点也在等待,当前节点应该被阻塞,则返回true

如果前驱节点被取消,则循环往前找到未被取消的节点,并将其作为前驱节点,然后返回false,当前线程不被阻塞,会再次执行acquireQueued中的循环

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)  // 前驱节点等待中
        return true;
    if (ws > 0) {   // 前驱节点被取消
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {    // 前驱节点不是等待中/被取消,则将前驱节点的状态更新为SIGNAL(-1)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

ReentrantLock解锁

ReentrantLock的解锁方法相对较简单,整体步骤大致分为以下两步

  1. 判断是否是当前线程持有锁,如果是,则先把同步器中持有线程置空,并且把state更新为0
  2. 唤醒CLH队列中后继节点的等待线程

image-20230225200734003

将exclusiveOwnerThread置空,state置0逻辑在ReentrantLock.Sync#tryRelease中实现

// ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())    // 校验当前线程是否是持有同步器的线程
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);      // 先将同步器中持有线程置空
    }
    setState(c);                            // state置0
    return free;
}

唤醒后继节点逻辑在AQS模板中已经实现了

// AQS#unparkSuccessor
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 后继节点waitStatus<0,则先把等待状态置为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    // 如果后继节点是null或后继节点的waitStatus是取消,则从tail节点一直往前找到第一个等待的节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果存在等待的节点线程,则将其唤醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

总结

AQS是实现锁或者同步组件的关键类,它面向的是锁的实现则,并且减法了实现方式,简化了同步状态管理、现成的排队、等待与唤醒等底层操作。它隔离了锁和同步组件使用者和实现者所关注的领域。我们在开发中很少会直接使用AQS,学习AQS对我们理解JUC锁和同步组件有很大的帮助。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情