循序渐进理解AQS(2):AQS实现(未完)

701 阅读5分钟

概述

上一篇 我们已经讨论了锁的实现思路,那么AbstractQueuedSynchronizer(以下简称AQS)和前面的实现有不同?

  1. 当线程获取不到锁时,AQS使用自旋和阻塞;
  2. 为了支持取消和超时操作,AQS对CLH锁的队列进行了改进,增加显式的链接指向前继节点。如果直接前继节点取消或者超时了,就寻找直接前继的前继;
  3. 由于释放锁需要通知后继节点,AQS又增加了后继节点链接进行优化(非必要)。

功能

一个同步器一般需要包含以下两个操作:

  1. 获取操作:acquire

阻塞调用的线程,直到同步状态允许其继续执行。

while (同步状态获取失败) {
  如果当前线程还未入队,则加入队列;
  阻塞当前线程;
}
如果当前线程在队列中,则移除
  1. 释放操作:release

通过某种方式改变同步状态,使得一或多个被acquire阻塞的线程继续执行。

更新同步状态
if (同步状态许可一个阻塞线程进行获取) {
  唤醒一个或多个队列中的线程
}

因此我们可以从这两个接口入手对源码进行解读。另外需要补充说明的是,锁的实现可以分为独占锁和共享锁,简单起见,我们先聚焦独享锁的代码实现,后续再看共享锁的差异性。

源码解读

acquire

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. 尝试获取锁,获取成功直接返回,不执行后续操作;
  2. 创建代表当前线程的节点,加入等待队列;
  3. 自旋前继节点状态和阻塞线程
tryAcquire

该方法检查同步状态state是否被许可,通俗来讲就是看看是否能取到锁。AQS中的实现只抛出异常,所以基于AQS实现的锁需要实现这个方法。

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
Node

当同步状态没有被许可时,需要在等待队列中排队,因此需要创建一个代表该线程的节点加入队列。下面我们来看节点的定义(删减了部分目前无须关注的属性)。

static final class Node {
    static final Node EXCLUSIVE = null;

    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    
    volatile int waitStatus;
    volatile Thread thread;
}
  • EXCLUSIVE:表示节点类型是独占锁。
  • waitStatus:描述状态,目前我们只关注CANCELLED(由于超时或线程中断导致取消等待)和SIGNAL(表示如果该节点释放了锁,需要通知后继节点,后继节点在等待被唤醒)两种状态。
  • thread:节点对应的线程。
addWaiter

接下来需要把节点加入到等待队列,整体思路是在队尾插入节点。

入队的时候需要考虑队尾为空和不为空两种情况,不过AQS的实现上是认为多数情况下队尾都不为空,因此先按照队尾不为空的方式尝试快速入队,如果失败才用完整的入队逻辑去入队。

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

初始化队列的头节点和尾节点:

compareAndSetHead

在队尾插入新节点,addWaiter中快速插入新节点的路径就是这块逻辑:

addWaiter

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;
            }
        }
    }
}
acquireQueued

节点加入队列后,接下来要做的事就是不断检查状态是否可用。这里实现的思路是先看前继节点是否是头节点(因为只有头节点释放锁后,后继节点才有可能获取到锁),然后再去检查状态;如果前继不是头节点,则修改前继节点的状态waitStatus = SIGNAL(表示后继在等待唤醒),然后阻塞线程。

如果头节点的后继成功获取到锁了,则头节点可以出队了:

  1. 修改头节点的指向到新节点(原头节点的后继);
  2. 新头节点的前继prev置为null(新头节点的前继就是原头节点)

为了帮助GC回收原头节点,把原头结点的后继也置为null

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);
    }
}

接下来是释放锁的操作,从节点入队的流程来看,释放锁时除了需要修改同步状态status,还需要唤醒后继节点。

release

整个实现主要涉及下面三个事情:

  • 修改同步状态
  • 检查是否有后继节点需要唤醒
  • 唤醒后继节点
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
unparkSuccessor

unparkSuccessor(h)

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    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);
}