按部就班理解AQS(2):AQS实现(未完)

概述

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

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

功能

一个同步器通常须要包含如下两个操做:bash

  1. 获取操做:acquire

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

while (同步状态获取失败) {
  若是当前线程还未入队,则加入队列;
  阻塞当前线程;
}
若是当前线程在队列中,则移除
复制代码
  1. 释放操做:release

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

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

所以咱们能够从这两个接口入手对源码进行解读。另外须要补充说明的是,锁的实现能够分为独占锁和共享锁,简单起见,咱们先聚焦独享锁的代码实现,后续再看共享锁的差别性。优化

源码解读

acquire

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

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

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
复制代码
Node

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

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

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

入队的时候须要考虑队尾为空和不为空两种状况,不过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

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

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);
}
复制代码
相关文章
相关标签/搜索