JUC:并发基础之AQS

538 阅读25分钟

什么是AQS

AQS是AbstractQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock、ReentrantReadWriteLock、CountdowLatch等就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。

AQS是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合AQS,利用AQS实现锁的语义。

可以这样理解AQS和锁的关系:

  • 锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节
  • AQS面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

AQS的接口

AQS的设计是基于模板方法模式的,也就是说,使用者需要继承AQS并重写指定的方法,随后将AQS组合在自定义同步组件的实现中,并调用AQS提供的模板方法,而这些模板方法将会调用使用者重写的方法。

访问或修改同步状态

AQS提供了三个原子方法来访问或者修改同步状态。

  • getState():获取当前的同步状态
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect, int update):使用CAS设置当前状态。

AQS维护一个volatile变量来管理同步状态的更改,所以getState()方法有volatile读的内存原语,setState()方法有volatile写的内存原语,compareAndSetState()方法则同时拥有volatile读写的内存原语。

题外话:volatile内存原语

volatile能保证可见性和有序性。

可见性就不说了。看看volatile禁止哪些操作重排序。

  • 当第二个操作是volatile写时,不管第一个操作时什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作时volatile读时,不能重排序。

也就是说。volatile内存语义禁止了getState()方法与后面的操作重排序、setState()与前面的操作重排序,compareAndSetState()则禁止了前后操作都不能重排序。

AQS可重写的方法

  • protected boolean tryAcquire(int arg):独占获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态。
  • protected boolean tryRelease(int arg):独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
  • protected int tryAcquireShared(int arg):共享是获取同步状态,返回大于等于0的值,表示获取成功,反之则获取失败。
  • protected boolean tryReleaseShared(int arg):共享式释放同步状态。
  • protected boolean isHeldExclusively():当前AQS是否在独占模式下被线程占用,一般该方法表示是否被当前线程独占。

实现自定义同步组件时,将会调用AQS提供的模板方法,模板方法则会调用实现者重写的上面几个方法。

AQS提供的模板方法

  • void acquire(int arg):独占获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法。
  • void void acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException并返回。
  • boolean tryAcquireNanos(int args, long nanos):在acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时实践内没有获取到同步状态,那么将会返回false,如果获取到了返回true。
  • void acquireShared(int arg):共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别式在同一时刻可以又多个线程获取到同步状态。
  • void acquireSharedInterruptibly(int arg):与acquireShared(int arg)相同,该方法响应中断。
  • boolean tryAcquireSharedNanos(int arg, int nanos):在acquireSharedInterruptibly(int arg)基础上增加了超时限制。
  • boolean release(int arg):独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。
  • boolean release(int arg):共享式的释放同步状态。
  • Collection getQueuedThreads():获取等待在同步队列上的线程集合。

AQS实现分析

AQS依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

先来看看AQS的数据结构。

//队列头节点
private transient volatile Node head;
//队列尾节点
private transient volatile Node tail;
//代表共享资源
private volatile int state;

如上图,AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

Node结构

    /** 标识节点当前在共享模式下 */
    static final Node SHARED = new Node();
    /** 标识节点当前在独占模式下 */
    static final Node EXCLUSIVE = null;
    /**
     * 由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待。
     * 节点进入该状态将不会变化。
     * 代表了该线程不再参与争抢锁。
     */
    static final int CANCELLED =  1;
    /**
     * 后续节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,
     * 将会通知后继节点,使后继节点的线程得以运行。
     */
    static final int SIGNAL    = -1;
    /**
     * 节点在等待队列中,节点线程等待在Condition上。
     * 当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,
     * 加入到对同步状态的获取。
     */
    static final int CONDITION = -2;
    /**
     * 表示下一次共享式同步状态获取将会无条件地被传播。
     */
    static final int PROPAGATE = -3;
    /**
     * 初始状态时值为0
     */
    volatile int waitStatus;
    /**
     * 同步队列的前驱节点
     */
    volatile Node prev;
    /**
     * 同步队列的后继节点
     */
    volatile Node next;
    /**
     * 获取同步状态的线程
     */
    volatile Thread thread;
    /**
     * 等待队列中的后继节点。
     * 如果当前节点时共享的,那么这个字段将是一个SHARED常量。
     * 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
     */
    Node nextWaiter;

节点是构成同步队列(等待队列)的基础,AQS拥有首节点(head)和尾节点(tail),没有成功获取到同步状态的线程将会成为节点加入该队列的尾部。

独占模式同步状态获取

acquire

通过调用AQS的acquire(int arg)方法可以获取同步状态,该方法在等待状态中不会被中断不会抛出异常,但是会将中断状态补上,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列移除。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 补中断状态
            selfInterrupt();
    }

tryAcquire

tryAcquire是由使用者来实现获取同步状态的具体逻辑。

addWaiter

当tryAcquire方法获取同步状态失败时,会先调用addWaiter。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 快速尝试在尾部添加
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果添加失败则调用enq
        enq(node);
        return node;
    }
  • 首先尝试再尾部添加Node
  • 如果添加失败则调用enq方法
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 如果没有尾节点
            if (t == null) {
                // 则new一个空节点设置为头节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 在尾部添加节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

轮询的方式插入尾节点。 添加成功则将当前节点返回。

acquireQueued

接下来调用acquireQueued方法。

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取当前node的前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点是头节点则调用tryAcquire(&&短路)
                // tryAcquire是提供给用户自己实现的
                if (p == head && tryAcquire(arg)) {
                    // 如果当前节点的头节点为前驱节点且获取到了同步资源
                    // 将当前节点设置为头节点
                    setHead(node);
                    // 取消前面节点对当前节点的引用,以便当前节点GC
                    p.next = null; 
                    // 将失败状态设置为false
                    failed = false;
                    // 返回中断状态
                    return interrupted;
                }
                // 如果当前节点的前驱节点不是头节点或者tryAcquire获取同步资源失败,则会走到这里
                // shouldParkAfterFailedAcquire方法会设置node状态以及返回是否需要阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 如果需要阻塞阻塞当前线程,并返回中断状态
                    // 阻塞中是否有外部线程设置过当前线程中断状态
                    parkAndCheckInterrupt())
                    // 如果需要被中断则将中断状态设置为true
                    interrupted = true;
            }
        } finally {
            // 如果发生异常失败有一个收尾操作
            if (failed)
                cancelAcquire(node);
        }
    }
  • 判断前驱节点是否头节点
    • 是头节点则调用tryAcquire尝试获取同步资源
      • 获取同步状态成功则将自己设置为头节点
    • 不是头节点或者获取同步状态失败则判断当前线程是否需要阻塞,如果需要阻塞就将当前线程阻塞
  • 如果failed状态位为flase,则做收尾工作

shouldParkAfterFailedAcquire

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取前驱节点的等待状态
        int ws = pred.waitStatus;
        // 如果前驱节点等待状态为SINGAL表明需要阻塞当前节点
        if (ws == Node.SIGNAL)
            return true;
        // 如果前驱节点等待状态大于0(CANCELLED)表明前驱节点已经不再参与竞争锁
        // 则需要将从前驱节点往前找,找到一个可用的前驱节点
        // 也就是说状态为CANCELLED的节点将从队列中删除并GC
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 如果前驱节点状态值为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  • 如果前驱状态节点状态为SIGNAL则返回需要阻塞
  • 如果前驱节点状态为CANCELLED,则跳过已经取消的前驱节点,并删除已经取消的前驱节点,返回不需要阻塞
  • 如果前驱节点状态为默认值,则设置为SIGNAL返回不需要阻塞

cancelAcquire

    private void cancelAcquire(Node node) {
        if (node == null)
            return;
        // 将当前节点指向的线程设置为null
        node.thread = null;
        // 获取前驱节点
        Node pred = node.prev;
        // 跳过已取消的前驱节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        Node predNext = pred.next;
        // 将当前节点状态设置为已取消
        node.waitStatus = Node.CANCELLED;
        // 如果当前节点是尾节点,将最后一个状态不是已取消的节点设置为尾节点
        if (node == tail && compareAndSetTail(node, pred)) {
            // 并将尾节点的后继节点设置为null
            compareAndSetNext(pred, predNext, null);
            // 当前节点如果不是尾节点或者设置失败
        } else {
            int ws;
            // 如果前驱节点不是头节点
            if (pred != head &&
                // 并且前驱节点的状态为SIGNAL
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                // 或者前驱节点是可用的并且设置前驱节点的等待状态为SIGNAL成功
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                // 并且前驱节点的指向的线程不为空
                pred.thread != null) {
                Node next = node.next;
                // 如果当前节点的后继节点不为null且后继节点等待状态为可用
                if (next != null && next.waitStatus <= 0)
                    // 则设置前驱节点的后继指向后继节点(也就是删除当前节点)
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 唤醒后继节点
                unparkSuccessor(node);
            }
            node.next = node; 
        }
    }

取消正在进行的获取尝试。

unparkSuccessor

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // 如果节点状态为可用,则设置节点状态为默认值
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 获取后继节点
        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);
    }

该方法用于唤醒后继节点。

独占模式获取资源流程图

独占模式释放资源

release

该方法会作为模板方法,会调用者实现的tryRelease释放资源,随后唤醒后继节点。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
  • tryRelease:该方法由使用者自行实现释放资源逻辑。
  • 如果释放资源成功,且当前头节点状态不为默认状态,则唤醒后继节点。

release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

共享模式获取资源

acquireShared

通过调用AQS的acquireShared(int arg)方法可以共享式地获取同步状态。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
  • 首先会调用tryAcquireShared方法共享式获取资源,该方法由使用者实现。
  • 返回结果大于等于0则表示共享资源获取成功,调用doAcquireShared方法。

doAcquireShared

    private void doAcquireShared(int arg) {
        //调用addWaiter方法生成SHARED模式的Node并插入同步队列未必
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取当前节点的前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点为头节点
                if (p == head) {
                    // 则再次尝试获取共享资源
                    int r = tryAcquireShared(arg);
                    // 返回结果大于等于0则表示获取同步资源成功
                    if (r >= 0) {
                        // 将当前节点设置为头节点,如果还有剩余资源则唤醒后继线程
                        setHeadAndPropagate(node, r);
                        // 将头节点指向当前节点的指针取消
                        // 我认为是为以后当前节点GC的时候提供帮助 这里有更好的理解麻烦指导一下
                        p.next = null; 
                        // 补上中断状态
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 判断是否需要阻塞以及阻塞自己等待前驱节点释放共享资源唤醒
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 设置中断状态
                    interrupted = true;
            }
        } finally {
            // 循环异常结束且获取同步状态失败的收尾工作
            if (failed)
                cancelAcquire(node);
        }
    }

流程和独占模式的acquire大抵相似,只不过是把补中断放到doAcquireShared里来了,如果更新之后的共享资源大于0,还需要唤醒后面的等待节点去获取资源。

setHeadAndPropagate

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head;
        // 设置当前节点为头节点
        setHead(node);
        // 如果更新之后的共享资源大于0
        // 或者之前的头节点为null 或者 之前的头节点等待状态为可用状态
        // 或者当前头节点为null 或者当前头节点等待状态为可用状态
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            // 且下一个节点状态为共享状态
            if (s == null || s.isShared())
                // 然后进行共享模式的释放
                // 实际是唤醒下一个节点
                doReleaseShared();
        }
    }

设置头节点的同时,如果当前头节点或者之前的头节点waitStatus为可用状态,还需要唤醒后继阻塞的节点。

doReleaseShared

private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 如果头节点等待状态为SIGNAL
                if (ws == Node.SIGNAL) {
                    // 将头节点等待状态设置为默认状态
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;
                    // 唤醒后继节点
                    unparkSuccessor(h);
                }
                // 如果头节点等待状态为0
                else if (ws == 0 &&
                         // 将头节点状态设置为PROPAGATE
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            if (h == head)          
                break;
        }
    }
  • 如果当前节点状态为SIGNAL(可唤醒后继节点),CAS方式将节点状态设置为默认状态,然后唤醒后继节点。
  • 如果当前节点状态为默认状态,则CAS方式设置状态为PROPAGATE(表示下一次共享式同步状态获取将会无条件地被传播)。

共享获取资源小结

在doAcquireShared中,CAS方式获取共享资源如果有剩余,则会唤醒下一个节点,下一个节点会尝试去获取资源。

那么问题来了,比如Status=10,A线程已经拿到了5个资源,B线程需要6个资源,C线程只需要1个资源。 这个时候A线程拿到资源后会唤醒B线程,B资源CAS去获取资源,一看资源不够,这个时候会阻塞自己,但是不会唤醒C线程,也就是说进入队列的线程是严格按照顺序去获取资源的(这里牺牲了一定的性能)。

共享资源释放

releaseShared

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
  • 调用使用者实现得tryReleaseShared方法,当status剩余大于0时则返回ture
  • 接着唤醒后继线程

共享资源释放小结

释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。

独占式超时获取同步状态

tryAcquireNanos

该方法会响应中断,和acquireInterruptibly、acquireSharedInterruptibly(流程和上面的acquire、acquireShared大抵一致)一样,在等待同步状态的时候,如果当前线程被中断,会立即返回,并抛出InterruptedException异常。同时在nanosTimeout内如果没有获取到同步状态,会返回false。

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }
  • 如果线程被外部中断,则直接抛出InterruptedException异常。
  • 调用使用者实现的tryAcquire获取同步资源。
  • 获取同步资源如果失败,则调用doAcquireNanos方法。

doAcquireNanos

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        // 初始化结束时间
        final long deadline = System.nanoTime() + nanosTimeout;
        // 初始化独占式的Node并入队尾
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                // 获取Node的前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点为头节点再次尝试获取资源
                if (p == head && tryAcquire(arg)) {
                    // 获取资源成功则将当前节点设置为头节点
                    setHead(node);
                    // 帮助GC
                    p.next = null; 
                    failed = false;
                    return true;
                }
                // 计算线程阻塞时间
                nanosTimeout = deadline - System.nanoTime();
                // 如果阻塞时间小于等于0则表示阻塞已经超时,直接返回falseif (nanosTimeout <= 0L)
                    return false;
                // 判断是否需要阻塞(寻找安全点)
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 如果超时时间只剩1秒了则不需要阻塞了
                    nanosTimeout > spinForTimeoutThreshold)
                    // 阻塞时间计算的计算时间
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                // 收尾
                cancelAcquire(node);
        }
    }

和acquire大抵类似,只是加上了超时时间的控制。

共享方式的超时获取资源方法也大抵类似,这里就不进行介绍了。

Condition

每一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用放上以及功能特性上还是有差别的。

Object的监视器方法与Condition接口的对比(摘自《Java并发编程的艺术》)

名词定义

同步队列:调用acquire获取同步资源失败则进入同步队列。 等待队列:调用await方法则进入等待队列。

Condition接口

Condition接口定义了如下方法:

    /**
     * 当前线程进入等待状态,直到被通知(signal)或中断
     */
    void await() throws InterruptedException;
    /**
     * 当前线程进入等待状态,直到被通知(signal),对中断不敏感
     */
    void awaitUninterruptibly();
    /**
     * 当前线程进入等待状态,直到被通知(signal)、中断或超时
     * 返回值为实际耗时
     */
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    /**
     * 当前线程进入等待状态,直到被通知(signal)、中断或超时
     */
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    /**
     * 当前线程进入等待状态,直到被通知(signal)、中断或到达deadline
     */
    boolean awaitUntil(Date deadline) throws InterruptedException;
    /**
     * 唤醒一个等待在Condition上的线程
     */
    void signal();
    /**
     * 唤醒所有等待在Condition上的线程
     */
    void signalAll();

ConditionObject数据结构

        // 等待队列头节点
        private transient Node firstWaiter;
        // 等待队列尾节点
        private transient Node lastWaiter;
        // 表明在signal之后发生了中断,但在后续不抛出中断异常,而是“补上”这次中断
        private static final int REINTERRUPT =  1;
        // 表明在signal之前发生了中断发生了中断,且在后续需要抛出中断异常
        private static final int THROW_IE    = -1;
        

可以看到沿用了AQS的结点

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)) {
                // 挂起线程
                LockSupport.park(this);
                // 检查当前线程在wait状态中是否被外部线程中断
                // 如果被外部中断,该方法CAS方式将节点状态置为0加入到同步队列,并返回THROW_IE,表示需要在后续抛出中断异常
                // 如果CAS失败表明该线程已经被别的线程唤醒,则返回REINTERRUPT,表示后续要补上中断位
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 走到这里表明已经进入同步队列了,尝试获取同步资源
            // 如果获取到同步资源且返回的中断状态为ture,且当前中断状态不为THROW_IE,则表明当前线程在wait状态中并没有被中断
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 如果当前节点的后继节点不为null,要清除等待队列中等待状态不为CONDITION的节点
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            // 如果发生了中断
            if (interruptMode != 0)
                // 如果是在wait状态中发生了中断则抛出异常
                // 如果被唤醒后发生了中断则补上中断
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            if (t != null && t.waitStatus != Node.CONDITION) {
                // 等待队列中删除等待状态不是CONDITION的节点
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 创建当前线程节点,状态为CONDITION。
            Node node = new Node(Thread.currentThread(), Node.CONDITION。);
            // 如果头节点为null,则当前节点为头节点。
            if (t == null)
                firstWaiter = node;
            else
            // 否则尾插
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

该方法不需要CAS同步,因为调用await的前置条件就是调用acquire已经拿到了同步资源并进入同步代码块。

unlinkCancelledWaiters

删除等待队列等待状态不是CONDITION的节点

        private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

从首节点开始,如果当前等待状态不为CONDITION则删除当前节点。 trail指向当前节点的前面一个节点,如果trail为空则表明firstWaiter状态为CONDITION。

fullyRelease

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            // 获取当前同步资源 
            int savedState = getState();
            // 释放同步资源并唤醒后继线程,返回释放的同步资源。
            if (release(savedState)) {
                failed = false;
                return savedState;
            // 如果当前线程没有获取同步资源,也就是没有拿到锁,则抛出异常
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

释放同步资源并唤醒后继节点。

isOnSyncQueue

    final boolean isOnSyncQueue(Node node) {
        // 如果节点等待状态为CONDITION或者节点没有前驱节点,则证明该节点一定不在同步队列中
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        // 如果节点有后继节点,则表明该节点一定在同步队列中
        if (node.next != null) 
            return true;
        // 从同步队列尾部查找该节点是否在同步队列中
        return findNodeFromTail(node);
    }

判断当前节点是否在同步队列中。

findNodeFromTail

    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

从同步队列尾部查找该节点是否在同步队列中。

await方法总结

  • 首先校验中断位,如果进入wait方法发生了中断需要抛出InterruptedException异常
  • 创建等待状态为CONDITION的节点并尾插入等待队列,同时会清理掉等待队列中状态不为CONDITION的节点
  • 释放同步资源,并唤醒后继线程,在这个地方保存释放的同步资源
  • 循环判断当前节点是否在同步队列中,如果在同步队列中则表明该节点被其他线程signal并进入到同步队列
    • 如果不在同步队列中则表明需要阻塞,阻塞当前线程
    • 当节点被唤醒则需要判断当前节点在wait状态中是否被中断
      • 如果被中断,则需要响应中断,从wait状态中回来
        • CAS方式将节点状态设置为默认,并加入到同步队列,返回THROW_IE表明是在wait状态中中断的,需要补上InterruptedException异常
        • 如果CAS设置节点等待状态失败,则表明当前线程被外部线程signal唤醒过,则需要重复判断当前节点是否在同步队列中,直到当前节点在同步队列中才返回REINTERRUPT
  • 从上一步退出表明当前线程已经进入同步队列,也就是说可能被外部线程唤醒也可能被外部线程中断(这里要注意被中断的线程并不是当场就处理中断,而是要获取到同步资源才抛出中断异常)
  • 调用acquireQueued获取同步资源,返回中断状态
    • 如果在acquireQueued过程中线程被中断且中断状态不为THROW_IE,则将中断状态设置为REINTERRUPT,表明后面要补上中断位
  • 清理一下等待队列
  • 最后根据中断状态判断是补上中断状态位还是抛出InterruptedException异常

signal

唤醒任意一个其他线程。

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
  • 如果非独占模式,直接抛异常。
  • 如果等待队列首部不为空,则调用doSignal。

doSignal

            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
  • 删除头节点。
  • 调用transferForSignal

transferForSignal

    final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
  • CAS方式改变当前节点状态为初始状态
  • 如果改变成功则将当前节点加入到同步队列
  • 拿到当前节点等待状态,如果等待状态大于0表示已经放弃获取同步资源或者CAS设置状态失败才唤醒当前线程(这里要注意,这种情况十分少见,正常情况下只是将当前等待节点加入到同步队列中排队,而不唤醒当前等待节点指向的线程,等到某个线程调用release方法,然后唤醒后继节点的时候当前等待线程才会被唤醒进入到await方法中获取锁)。

signal总结

  • 获取等待队列首节点,将该节点从等待队列中移除。
  • 将该节点加入到同步队列。
  • 如果当前节点状态被其他线程修改或者CAS方式改变节点状态为SIGNAL(这是一种很特殊的现象,有同学分析一下怎么重现这种情况吗)失败,则认为这是暂时的、无害的错误(官方注释),直接唤醒当前线程。

AQS总结

  • 两个队列:同步队列和等待队列(公用一个Node节点内部类)
  • 一个资源:status。