java并发基石AQS

94 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

前言

AQS全称为AbstractQueuedSynchronizer,它提供了一个FIFO(First in First out 先入先出)队列,底层实现的数据结构是一个双向链表。可以说是 J.U.C 并发包里大多数工具的基石,常见的有:ReentrantLock、CountDownLatch、CyclicBarrier等。AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。

20190203234437159.png

AQS 支持两种模式:

  • 独占模式:同一时刻只允许一个线程访问共享资源
  • 共享模式:同一时刻允多个线程访问
  • 公平模式:获取锁失败的线程需要按照顺序排队,先进先出
  • 非公平模式:当线程需要获取锁时,会尝试直接获取锁

AQS对接节点数据结构Node

static final class Node {
    /** 共享模式 */
    static final Node SHARED = new Node();
    /** 独占模式 */
    static final Node EXCLUSIVE = null;

    /** 表示当前的线程被取消 */
    static final int CANCELLED =  1;
    /** 表示当前节点的后继节点包含的线程需要运行,需要unparking */
    static final int SIGNAL    = -1;
    /** 表示当前节点线程在等待condition */
    static final int CONDITION = -2;
    /**
     * 指示下一个acquireShared应该无条件传播
     */
    static final int PROPAGATE = -3;

    /**
     * 表当前节点的等待状态值,取值为上面的四个常量
     */
    volatile int waitStatus;

    /**
     * 前驱节点
     */
    volatile Node prev;

    /**
     * 后继节点
     */
    volatile Node next;

    /**
     * 节点封装的线程
     */
    volatile Thread thread;

    /**
     * 链接到下一个等待条件的节点
     */
    Node nextWaiter;

    /**
     * 是否共享模式.
     */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
     * 返回此节点的前一个节点
     */
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {   
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

可以看出,该内部类是一个双向链表,保存前后节点,然后每个节点存储了当前的状态waitStatus、当前线程thread,还可以通过SHAREDEXCLUSIVE两个变量定义为共享模式或者独占模式。

独占模式

首先我们来分析独占模式,独占模式作为最常用的模式使用范围很广,比如ReentrantLock,加锁和释放锁就是使用互斥模式来实现的。 独占模式中核心加锁方法是acquire():

/**
 * 以独占模式获取,忽略中断。通过调用至少一次tryAcquire来实现,并在成功后返回。
 * 否则线程将排队,可能会反复阻塞和取消阻塞,调用tryAcquire直到成功。
 * 此方法可用于实现方法锁定。
 */
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

其中tryAcquire()方法是没有具体实现的,需要继承者自己实现。tryAcquire()方法返回成功或者失败,如果失败之后先执行addWaiter()添加一个独占式的节点:

/**
 * 将点前线程包装的node根据给的的模式添加到队列
 */
private Node addWaiter(Node mode) {
    Node node = new Node(mode); //创建一个节点,此处mode是独占式的。

    for (;;) {// 注意这里是循环
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);// 如果 tail 节点不是 null,就将新节点(node)的前节点设置为 tail 节点,并且将新节点(node)设置成 tail 节点。
            if (compareAndSetTail(oldTail, node)) {// CAS 将 tail 更新为新节点(node)
                oldTail.next = node;// 把原 tail 的 next 设为 node。至此,完成了把新节点 node 插入到原来尾节点的后面,并设置成新的尾节点。
                return node;
            }
        } else {
            initializeSyncQueue();// 还没有初始化,就调用 initializeSyncQueue() 方法
        }
    }
}

/**
 * 初始化头结点和尾节点
 */
private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        tail = h;
}

至此,我们添加了一个新的节点到原来的队列,并且把新加入的节点设置成了尾节点。然后看acquireQueue()方法:

/**
 * 以独占不中断模式获取已在队列中的线程
 * 用于条件等待方法和获取。
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node); // 取消加锁,恢复状态
        if (interrupted) // 响应中断
            selfInterrupt();
        throw t;
    }
}

/**
 * 用来挂起当前的线程,返回中断标志
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

/**
 * Cancels an ongoing attempt to acquire.
 *
 * @param node the node
 */
private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;// node节点内的线程置为空

    // Skip cancelled predecessors
    Node pred = node.prev;    // pred 是前驱节点
    while (pred.waitStatus > 0)// 找到 pred 结点前面最近的一个状态不为 CANCELLED 的结点
        node.prev = pred = pred.prev;

    Node predNext = pred.next;

    node.waitStatus = Node.CANCELLED;//当前节点的状态改成 CANCELLED

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {//如果当前节点是尾节点,则利用 CAS 设置尾结点为 pred 结点
        pred.compareAndSetNext(predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        //如果 pred 结点不为头结点
        //并且(pred 结点的状态为 SIGNAL 或者 (ws 小于 0 并且 CAS 设置等待状态为 SIGNAL 成功))
        //并且 pred 结点内的线程不为空
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL || 
             (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)//后继节点不为空 并且后继节点的等待状态小于等于0
                pred.compareAndSetNext(predNext, next);//把当前节点的后节点设置成本节点的后节点,也就是说把本节点剔除出去。
        } else {
            unparkSuccessor(node);// 释放节点的后继节点
        }

        node.next = node; // help GC
    }
}

shouldParkAfterFailedAcquire()(注意该方法是在循环里面) 这个方法最终会返回true或者false,从这个方法的名称可以看出,该方法的作用是在当前线程获取资源失败后是否挂起当前线程,显然:

  • 返回true,说明前驱节点的waitStatus==-1,是正常情况,那么当前线程需要被挂起,等待以后被唤醒。当前节点是被前驱节点唤醒,就等着前驱节点拿到锁,然后释放锁的时候通知当前线程
  • 返回false,说明当前线程不需要被挂起,因为不符合挂起的条件。
/**
 * 检查并更新未能获取的节点的状态。
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;// ws是代表前节点的状态
    if (ws == Node.SIGNAL)// 前节点状态是等待唤醒状态,那么当前线程需要被挂起,等待以后被唤醒。
        return true;
    if (ws > 0) {//前节点状态是 CANCEL,代表可以忽略,我们删除掉这个节点,再看更前面的一个。
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {//前节点状态是0 或者 PROPAGATE,把状态改成 SIGNAL,但是不挂起。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  1. 尝试获取锁
  2. 获取不到锁的话,加入队列并将队列中的前元素的状态改为SIGNAL
  3. 如果出错,就恢复状态,抛出异常。
  4. 没有出错就按照需求判断是否需要中断,需要的话中断当前线程。

共享模式

共享模式的获取和释放锁的方法

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

需要注意到,获取锁除了基本的方法之外,还有两个增强的方法,这两个方法被用在SemaphoreCountDownLatchReentrantReadWriteLock中:

public final void acquireSharedInterruptibly(int arg)//在acquireShared 方法基础上增加了能响应中断的功能;
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)//在acquireSharedInterruptibly基础上增加了超时等待的功能
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquireShared(arg) >= 0 ||
        doAcquireSharedNanos(arg, nanosTimeout);
}

还是从获取锁开始看,tryAcquireShared方法跟tryAcquire类似,需要继承者手动实现,返回 0 代表当前线程能够执行,但之后的将会进入等待队列中;返回正数直接执行,之后的线程可能也可以直接执行。

我们还是先看实际主要逻辑所在的doAcquireShared方法:

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);//创建一个分享模式的节点,CAS 循环加到队尾,node 就是新加到队尾的那个节点。
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {//前节点是 head,证明当前节点是队列里的第一个。
                int r = tryAcquireShared(arg);
                if (r >= 0) {//获取锁成功
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    } finally {
        if (interrupted)
            selfInterrupt();
    }
}

这里与上面独占的部分也很相似,只有一个setHeadAndPropagate方法是新的,主要就是把当前节点设置成head节点,然后依次唤醒后续节点。

/**
 * 设置头节点并且进行后续唤醒
 */
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

下面这一大长串判断的逻辑是这样:首先propagate > 0代表当前线程已经获取到了资源,并且需要唤醒后面阻塞的节点;h.waitStatus < 0 代表旧的头节点后面的节点可以被唤醒;(h = head) == null || h.waitStatus < 0 这个操作是说新的头节点后面的节点可以被唤醒,总结来说:

  1. propagate > 0代表当前线程已经获取到了资源,并且需要唤醒后面阻塞的节点。
  2. 无论新旧头节点,只要其waitStatus < 0,那么其后面的节点可以被唤醒。

如果上面if返回true,接着获取当前节点的后继节点,这里又会有一个判断,如果后继节点是共享模式或者现在还看不到后继的状态,则都继续唤醒后继节点中的线程。上面if返回true,接着执行doReleaseShared方法,代码如下:

/**
 * 释放共享状态
 */
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {//如果状态是等待信号
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))//cas 操作失败的话就循环继续
                    continue;            // loop to recheck cases
                unparkSuccessor(h);// 唤醒后继节点
            }
            // 如果后继节点还未设置前驱节点的waitStatus为SIGNAL,代表目前无需唤醒或者不存在。
            // 那么就将头节点的waitStatus设置为PROPAGATE,代表在下次acquireShared时无条件地传播
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

再回头看doAcquireSharedInterruptiblydoAcquireSharedNanos方法,提供了可以中断和可以超时的获取锁方式:

/**
 * Acquires in shared interruptible mode.
 * @param arg the acquire argument
 */
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

可中断获取锁的逻辑跟前面acquire很像,唯一的区别是当parkAndCheckInterrupt返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。

通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回:

  1. 在超时时间内,当前线程成功获取了锁;
  2. 当前线程在超时时间内被中断;
  3. 超时时间结束,仍未获得锁返回false

具体实现如下:

/**
 * 指定超时时间获取共享状态
 */
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.SHARED);
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return true;
                }
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L) {
                cancelAcquire(node);
                return false;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

总结

在获得同步锁时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。底层依赖了CAS和LockSupport。