AQS之入队和出队过程

1,710 阅读8分钟

我们都知道在AQS内部维护着一个FIFO等待队列,而且只有当产生资源竞争的时候才会形成队列,那么它的过程究竟是怎样的呢?让我们来一探究竟吧!

我们来模拟一个情景:
假设现在 t1 线程已经通过 cas 持有锁,即 state = 1,exclusiveOwnerThread = t1,且此时 t2 想要获取锁,那么就会形成队列

入队过程

    private Node addWaiter(Node mode) {

        //将 t2 线程包装为node
        Node node = new Node(Thread.currentThread(), mode);

        //尝试进行一次入队操作,如果失败了就进入enq
        //在第一次入队的时候肯定是失败的,因为还没有初始化队列
        Node pred = tail;

        //如果存在尾节点,进入条件
        if (pred != null) {
            node.prev = pred;
            //如果存在队尾元素,则设置当前节点为尾节点
            if (compareAndSetTail(pred, node)) {
                //pred <- node
                pred.next = node;
                return node;
            }
        }
        //t2 enter queue
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        //会执行两次循环,第一次创建头节点,第二次将 t2 追加到头结点后面
        for (;;) {
            Node t = tail;
            //初始化队列,new 一个 Thread=null 的节点作为头结点
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //head <- t2 
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

第一次循环后,会创建一个 Thread = null 的头结点,且 head 和 tail 都指向头结点


第二次循环后,将 t2 追加到头结点后面,并且将尾指针指向 t2,头节点和 t2 变成双向链表
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获得 t2 的前驱节点
                final Node p = node.predecessor();
                //如果是头节点,则尝试获取锁,因为此时可能 t1 已经释放了锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果 t2 获取锁失败,那需要进入阻塞
                //注意!!该方法第一次进入时,t2 会把头节点的 waitStatus cas(0,-1)
                //并返回 false,在下次循环中才会真正阻塞 t2
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

第一次循环修改头节点的 waitStatus,由 0(default) -> -1(signal),表示节点出队后需要唤醒后续节点

第二次循环才调用 LockSuppurt.park(),真正将 t2 阻塞

此时,t2 的入队就完成了,总结一下就是:

  1. 将 t2 线程包装为 node 节点
  2. new 出一个 Thread = null 的头节点,将 t2 追加到头结点后面,建立引用关系
  3. 将头结点的 waitStatus 改为 signal,阻塞 t2

出队过程

   public final boolean release(int arg) {
           //set state=0,setExclusiveOwnerThread(null)
        if (tryRelease(arg)) {
            Node h = head;
            //存在头节点,且此时头节点的 waitStatus 等于 -1
            if (h != null && h.waitStatus != 0)
                //唤醒后继节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;
        if (ws < 0)
            //将头结点的waitStatus设为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;
        }
        //唤醒 t2
        if (s != null)
            LockSupport.unpark(s.thread);
    }

此时,t2 被唤醒后,又继续执行 acquireQueued 方法,进入循环

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获得 t2 的前驱节点
                final Node p = node.predecessor();
                //尝试获取锁成功,因为 t1 已经释放了锁,持有锁的线程变成 t2
                if (p == head && tryAcquire(arg)) {
                    //将 t2 设置为头节点,修改引用关系
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
             ...
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }



此时,t1 出队就完成了,总结一下就是:

  1. 修改 t1 的 waitStatus 为 0,避免多个线程同时进行唤醒操作
  2. 唤醒 t2 后,尝试获取锁成功,即 state = 1,exclusiveOwnerThread = t2
  3. 将 t2 设为头节点,head 指向 t2,并将 Thread = null