(三)手把手带你透彻理解AQS

119 阅读9分钟

1.口口相传的AQS到底是什么

AQS的完整类名是——AbstractQueuedSynchronizer(直译过来就是抽象队列同步器)AQS为Java中几乎所有的锁和同步器提供一个基础框架,派生出如ReentrantLock、Semaphore、CountDownLatch、线程池中的worker等。本文基于AQS原理的几个核心点

1.AQS如何定义资源:

  • Exclusive-独占,只有一个线程能执行,如ReentrantLock
  • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

2.两种队列

  • 同步等待队列: 主要用于维护线程获取锁失败时入队的线程
  • 条件等待队列: 线程在调用await()的时候会释放锁,然后线程会加入到条件等待队列,线程在调用signal()唤醒的时候会把条件队列中的线程节点再次移动到同步队列中,并且等待再次获得锁

3.四大特性

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

3.五种状态

  • 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
  • CANCELLED,值为1,表示当前的线程被取消;
  • SIGNAL,值为-1,处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行
  • CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
  • PROPAGATE,值为-3,共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点;

4.关键方法

不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

2.state状态如何维护

/**
 * The synchronization state. 
 */
private volatile int state;

/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState() {
    return state;
}

/**
 * Sets the value of synchronization state.
 * This operation has memory semantics of a {@code volatile} write.
 * @param newState the new state value
 */
protected final void setState(int newState) {
state = newState;
}


protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

注意下面三点:

  • state用volatile修饰,保持在线程中的可见性
  • getState和setState用final修饰,表示该方法在子类不可被覆盖
  • compareAndSwapInt使用CAS的思想来交换数据

3.CLH队列

CLH实际上是一个FIFO双向队列,队列中元素的类型为Node, Node里面封装的对象就是线程,AQS其实是基于这个队列来完成对同步状态state的管理,假设线程获取同步状态失败时,会把线程的相关信息封装成Node节点加入到CLH队列中,同时会阻塞当前线程。

CLH队列模型图

1.node节点内部结构

CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),condition队列的后续节点(nextWaiter)

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    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
     */
    static final int PROPAGATE = -3;
    //等待的状态
    volatile int waitStatus;
    //封装的线程
    volatile Thread thread;
    //用于condition里面调用 
    Node nextWaiter;
}

2.入队方法

1.acquire(int arg)

入口处统一调用acquire方法来获取锁

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

在tryAcquire方法中,AQS是没有实现的,这块留给子类去实现,这块子类实现的一个典型的例子就是ReentrantLock的公平锁和非公平锁的实现,后续这乱

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

2.addWaiter(Node mode)

private Node addWaiter(Node mode) {
//把前端的线程包装成节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 在初始的情况下,pred = tail=null
Node pred = tail;
if (pred != null) {
    node.prev = pred;
    if (compareAndSetTail(pred, node)) {
        pred.next = node;
        return node;
    }
}
//CAS循环插入节点
enq(node);
return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            //当tail不存在,其实就是首次插入时,会放入一个空节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
        	//把尾节点由t节点变为node节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

3.acquireQueued(final Node node, int arg)

下面的代码是插入队列,队列中等待的节点不会被中断。

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

对于可中断的插入节点

/**
 * Acquires in exclusive interruptible mode.
 * @param arg the acquire argument
 */
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //这块最大的区别,如果是中断,直接会抛出异常
                throw new InterruptedException();
        }
    } finally {
        //当抛出异常时,取消节点
        if (failed)
            cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            //如果前端的节点被取消,那么会一直向前面查找,并且把当前的节点挂在前 面的未取消的节点
            //之后
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        //如果当前状态没设置,会设置成SIGNAL
        //表示当前节点的后续节点通过park阻塞了
        //这块会修改前驱节点的状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    //返回线程是否中断
    return Thread.interrupted();
}

3.出队方法

1.release(int arg)

public final boolean release(int arg) {
//修改状态
if (tryRelease(arg)) {
    Node h = head;
    //head=null的情况只有一个线程进入,没有初始化队列,!=null至少说明队列被初始化过,但是是否 
    //有后续节点未知,waitStatus!=0说明下个节点是等待的
    if (h != null && h.waitStatus != 0)
        unparkSuccessor(h);
    return true;
}
return false;
}

2.unparkSuccessor(Node node)

unparkSuccessor环境之后阻塞监听的节点

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)
    //修改node节点的状态
    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;
//waitStatus>0表示下个线程是被cancel状态
//进这个是从队尾开始找,找最近的正常排队的线程
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);
}

4.Condition源码

  1. Condition#await方法会释放当前现成持有的锁,并且会阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候线程必须持有锁。
  2. Condition#signal方法会把Condition队列的首节点移动到阻塞队列尾部,并且会唤醒调用Condition#await方法而阻塞的线程(此处要注意:唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的前提是必须持有锁。

1.await()方法

public final void await() throws InterruptedException {
//若当前线程已经中断则抛出中断异常
if (Thread.interrupted())
    throw new InterruptedException();
//封装当前的节点,并且添加到等待队列
Node node = addConditionWaiter();
//释放当前线程所占用的锁,并保存当前锁的状态
int savedState = fullyRelease(node);
int interruptMode = 0;
//
while (!isOnSyncQueue(node)) {
    //如果当前node节点不在同步队列中,进行阻塞
    LockSupport.park(this);//这一行代码执行,当前线程就阻塞住了
    //执行到这一步说明当前线程被唤醒了
    //有可能是线程中断被唤醒
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}
//acquireQueued方法中,如果是队列的第一个等待节点,会再次争夺锁,否则插入到队列之中
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();
if (interruptMode != 0)
    //interruptMode == REINTERRUPT,则设置线程中断标识
    //interruptMode == THROW_IE 抛出线程中断异常
    reportInterruptAfterWait(interruptMode);
}

2.signal()方法

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //获取第一个等待者
    Node first = firstWaiter;
    
    if (first != null)
        doSignal(first);
}

 

private void doSignal(Node first) {
//不断沿着单向列表进行循环遍历
do {
    if ( (firstWaiter = first.nextWaiter) == null)
        lastWaiter = null;
    first.nextWaiter = null;
} while (!transferForSignal(first) &&
         (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
     * If cannot change waitStatus, the node has been cancelled.
     */
//修改线程节点的状态
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
    return false;

/*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
//插入到AQS队列之中
Node p = enq(node);
int ws = p.waitStatus;
//切换到可唤醒的状态
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
    LockSupport.unpark(node.thread);
return true;
}

5.为什么AQS同步队列要使用双向链表

AQS使用双向链表主要由以下3个原因

  1. 在没有竞争到锁的线程加入到阻塞队列的时候,需要判断前面节点是否是正常的状态,主要为了防止存在异常线程导致后续线程无法唤醒的问题。
  2. 当lock接口里面有一个lockInterruptibly()方法,这个方法表示在处于阻塞的情况是允许被中断的,当被中断的时候会把当前的节点修改为cancel状态,但是这个节点仍然存在列表中,意味想着在后续的锁竞争中,需要把这个节点从列表中删掉,假设此刻这个列表是单向列表,则新插入的节点需要从头遍历,性能很慢。
  3. 为了避免竞争的开销,对于加入到队列的队列的节点需要判断前面的是否是头节点,如果是头节点的下一个节点才有必要再次获取锁,否则,会插入到节点的尾部,这种可以避免羊群效应,如果是单向列表,需要从第一个节点去查找,会造成性能的损耗。