深入理解AQS

185 阅读10分钟

背景

AQS(AbstractQueuedSynchronizer)抽象的同步队列,内部通过维护一个volatile int state (共享资源)和一个FIFO等待队列。获取和释放同步队列的方法有子类负责实现。
AQS是一个抽象类,里面所有方法都默认实现,子类只需要重写具体方法即可。

框架

AQS定义两种资源获取方式:EXCLUSIVE(ReentrantLock)和SHARED(Semaphore和CountDownLatch等)。
不同的同步器实现时只需要实现共享资源的获取和释放方式即可,至于等待队列的维护AQS已经在顶层实现好了。自定义同步器可以根据需要实现如下方法:
isHeldExclusively:是否独占资源,用到conditions才需要实现它。
tryAcquire:独占方式获取锁,成功返回true,失败返回false。
tryRelease:独占方式释放锁,成功返回ture,失败返回false。
tryAcquireShared:共享方式获取锁,返回值等于0,表示当前线程获取锁成功,但不能继续唤醒后继线程。返回值小于0,表示没有资源可用。返回值大于0,表示当前线程获取锁成功,可以继续唤醒后继线程。
tryReleaseShared:共享方式释放锁,在CountDownLatch中状态值减为0的时候返回true,在Semaphore实现中当前线程释放成功则返回true。
以ReentrantLock为例,state初始为0,当有线程获取获取到锁时,会将state加1,表示为锁定状态。其它线程在获取就会失败,只有当前获取到锁的线程执行了unlock才会释放锁,state为0,其它线程才有机会获取锁。

源码

acquire(int arg)

独占模式获取锁的顶层入口,由AQS内部实现。如果获取到资源,线程直接返回,否则会进入等待队列,整个过程忽略中断。

public final void acquire(int arg) {
    /*
        tryAcquire 独占模式尝试获取锁,模板方法,有具体子类实现
        addWaiter 创建node节点
        acquireQueued 执行入队操作,该方法里面会尝试在获取一次锁,若获取失败,会被挂起
        selfInterrupt 补中断
     */
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

代码执行流程:

  1. 调用tryAcquire尝试获取锁,成功返回,失败执行acquireQueued
  2. 调用addWaiter创建node节点,标记为EXCLUSIVE,且加入到队列的尾部
  3. 调用acquireQueued,如果前驱为head,会在尝试获取锁,如果不是,找到安全点之后将自己挂起 在此过程中如果外部线程执行了中断,是不理会的,只有获取到资源之后才会自我中断,把中断补上。

tryAcquire(int arg)

尝试获取锁,成功返回true,失败返回false

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

初学者看到这里可能会有疑问,既然是空实现为啥不定义成abstract方法?
原因是:如果实现独占锁,只需要实现tryAcquire/tryRelease,如果实现共享锁,只需要实现tryAcquireShared/tryReleaseShared,如果定义成抽象方法,每个实现类都需要实现其它模式的接口。

addWaiter(Node mode)

把当前线程包装成Node,并加入到队列的尾部。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); //创建node节点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) { // 尝试快速加入尾部
            pred.next = node;
            return node;
        }
    }
    enq(node); // 自旋加入尾部,直到加入尾部才会退出
    return node;
}

enq(final Node node)

private Node enq(final Node node) {
    for (;;) { // 自旋
        Node t = tail; 
        if (t == null) { // 队列为空,也说明head和tail的创建时延迟创建,如果没有竞争,head和tail都是null
            if (compareAndSetHead(new Node())) // 创建个空节点,并让head指向它
                tail = head;
        } else { // 放入队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

第一次循环:
刚开始初始化,head是null,tail是null
创建个空节点new Node() head、tail同时指向 new node()
第二次循环:
把node的prev指向new Node(),node cas成tail,此时tail指向node
上一个tail的next指向node

因为cpu执行是分片执行,在某个时刻会造成双向链表不完整的情况:
时刻1:node.prev = t
时刻2:cas 成功
时刻3:执行其它功能
会造成node.next为null的情况

acquireQueued(final Node node, int arg)

通过tryAcquire获取资源失败,addWaiter加入队尾之后,下一步需要正式休息了,不过在休息之前,还需要做一些事情,要告知自己的前驱,前驱释放锁之后记得通知自己一下,做完这步就会真正去休息了。

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)) { // 如果是head且尝试获取锁成功,tryAcquire 模板方法,有具体子类实现
                setHead(node); // 设置head,同一时刻只有一个线程获取锁成功,这里setHead 是线程安全的,且一定能成功
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node); // 取消获取
    }
}

shouldParkAfterFailedAcquire(Node pred, Node node)

此方法是检查状态,看看是否真正可以去休息了,找到一个前驱是非取消的状态。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 获取前驱状态
    if (ws == Node.SIGNAL) // signal 可以挂起操作了
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev; // 如果前驱节点取消,会一直向前找,直到找到一个正常等待的节点
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);// 把前驱节点置成SIGNAL,意思就是释放锁之后记得唤醒我
    }
    return false;
}

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

parkAndCheckInterrupt()

如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 挂起
    return Thread.interrupted(); // 返回当前线程中断状态,interrupted会消除中断标志位
}

简单总结下获取资源流程:

  1. tryAcquire()资源,成功返回true,失败返回false
  2. 如果获取失败,执行addWaiter,并将当前线程封装成node节点,加入队列的尾部
  3. acquireQueued 使线程在队列中休息,休息之前在检查一次自己的前驱是否是head(代表当前获取锁的线程),如果是head,并且获取锁成功,会把自己设置成head,如果前驱不是head,会执行shouldParkAfterFailedAcquire
  4. shouldParkAfterFailedAcquire 循环找到一个前驱是非取消的节点,并把前驱的状态设置成SIGNAL(释放锁记得通知我)
  5. parkAndCheckInterrupt 挂起线程 以上就是互斥锁获取锁的线程,下面说下互斥锁的释放过程:

release(int arg)

互斥锁的释放顶层方法是release,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;
    }

tryRelease(int arg)

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

unparkSuccessor(Node node)

该方法的主要作用是,找到后继节点,并唤醒后继节点,使后继节点继续去争抢锁。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus; // 获取当前节点状态,当前node节点就是获取锁的节点
    if (ws < 0) // 如果ws状态小于0,置成0
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next; // 后继节点
    if (s == null || s.waitStatus > 0) {
        /*
            如果后继节点已经取消,就从tail向前找,找到一个正常的节点,此处之所以从tail向前遍历,就是end方法加入队列的时候,
            某个时刻某个节点没有后继节点,但前驱节点都是有的
         */
        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); // 唤醒线程
}

以上代码是互斥锁的获取和释放,相对来说还是比较简单的,下面会接着介绍共享锁的获取和释放过程。

acquireShared(int arg)

共享锁获取的顶层方法,对中断不敏感。

public final void acquireShared(int arg) {
    /*
        tryAcquireShared 模板方法,有子类负责实现
        < 0 没有可用资源,入队列等待
     */
    if (tryAcquireShared(arg) < 0) // 尝试获取锁
        doAcquireShared(arg);
}
/**
   * 共享模式获取锁 模板方法,实现类实现
   * 返回值:
   * 小于0 获取锁失败
   * 等于0 获取锁成功,但不能继续向下传播
   * 大于0 获取锁成功,可以继续向下传播
   * @param arg
   * @return
   */
  protected int tryAcquireShared(int arg) {
      throw new UnsupportedOperationException();
  }

tryAcquireShared 尝试获取锁,成功则直接返回,失败执行doAcquireShared加入等待队列,直至获取锁为止。

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED); // 创建等待节点 node
        boolean failed = true; // 是否发生错误
        try {
            boolean interrupted = false; // 外部是否执行中断操作,此处只是记录中断标志,当该线程获取锁之后,才会补中断操作,设置中断标识
            for (;;) {
                final Node p = node.predecessor(); // 前驱
                if (p == head) { // 若前驱是head,head表示获取锁的线程,只有前驱是head节点,才有机会去获取锁
                    int r = tryAcquireShared(arg); // 尝试获取锁,模板方法,有具体子类实现
                    /*
                        r 有三种情况:
                            返回负数,表示没有空闲锁
                            返回0,表示当前线程获取锁成功,但不能继续向下传播(不可以唤醒后驱节点)
                            返回大于0,表示当前线程获取锁成功,可以继续向下传播
                            tryAcquireShared 模板方法,有子类根据实际情况自己实现
                     */
                    if (r >= 0) {
                        setHeadAndPropagate(node, r); // 设置heaed并且继续向下传播
                        p.next = null; // help GC
                        if (interrupted) // 如果在park过程中,该线程被打断,就补中断异常
                            selfInterrupt(); // 自己打断自己
                        failed = false;
                        return;
                    }
                }
                /*
                    走到此处,说明当前节点的前驱节点不是head,会真正执行挂起操作
                    shouldParkAfterFailedAcquire 找安全点休息(会把当前节点的前驱waitstaus置成SIGNAL,此状态说明当前驱节点释放锁,负责唤醒后驱节点)
                    parkAndCheckInterrupt 挂起操作,且当被唤醒的时候返回中断状态
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node); //执行过程中如果被中断或者发生错误会取消改节点
        }
    }
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below  把当前head封锁到当前栈中,后面会使用
        setHead(node); // 设置head节点,把head执行node节点
      
        / * 时刻1:t3调用releaseShared -> doReleaseShared -> unparkSuccessor,完了之后head的等待状态为0
         * 时刻2:t1由于t3释放了信号量,被t3唤醒,调用Semaphore.NonfairSync的tryAcquireShared,返回值为0
         * 时刻3:t4调用releaseShared,读到此时h.waitStatus为0(此时读到的head和时刻1中为同一个head),将等待状态置为PROPAGATE
         * 时刻4:t1获取信号量成功,调用setHeadAndPropagate时,可以读到h.waitStatus < 0,从而可以接下来调用doReleaseShared唤醒t2
         */
        /*
            如果propagate传播值大于0 才会继续向下传播
            head 为null 说明没有竞争
            h.waitStatus 分为两种情况:
            1:SIGNAL
            2: PROPAGATE
         */
        if (propagate > 0 || h == null || h.waitStatus < 0) {
            /*
                node.next 为null并不是真正null,在中间时刻有可能双向链表处于断档状态,具体看end创建节点方法
                若不为null,后驱节点是共享节点 也执行唤醒操作
             */
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared(); // 释放共享锁
        }
    }

自己获取到锁之后,如果还有剩余资源,还需要唤醒后继节点,这就是共享锁和独占锁的区别。

private void doReleaseShared() {
        /*
            private static class Thread1 extends Thread {
            @Override
            public void run() {
                sem.acquireUninterruptibly();
            }
            }

            private static class Thread2 extends Thread {
                @Override
                public void run() {
                    sem.release();
                }
            }

            Thread t1 = new Thread1();
            Thread t2 = new Thread1();
            Thread t3 = new Thread2();
            Thread t4 = new Thread2();

         */


        /*
        head -> t1的node -> t2的node(也就是tail)

        信号量释放的顺序为t3先释放,t4后释放:
        时刻1: t3调用releaseShared,调用了unparkSuccessor(h),head的等待状态从-1变为0
        时刻2: t1由于t3释放了信号量,被t3唤醒,调用Semaphore.NonfairSync的tryAcquireShared,返回值为0
        时刻3: t4调用releaseShared,读到此时h.waitStatus为0(此时读到的head和时刻1中为同一个head),不满足条件,因此不会调用unparkSuccessor(h)
        时刻4: t1获取信号量成功,调用setHeadAndPropagate时(执行setHead之前,head的值还是时刻1的head),因为不满足propagate > 0(时刻2的返回值也就是propagate==0),从而不会唤醒后继节点

        加上PROPAGATE(传播)之后的情况:

        信号量释放的顺序为t3先释放,t4后释放:
        时刻1: t3调用releaseShared,调用了unparkSuccessor(h),head的等待状态从-1变为0
        时刻2: t1由于t3释放了信号量,被t3唤醒,调用Semaphore.NonfairSync的tryAcquireShared,返回值为0
        时刻3: t4调用releaseShared,读到此时h.waitStatus为0(此时读到的head和时刻1中为同一个head),不满足条件,因此不会调用unparkSuccessor(h),
        会执行ws=0 把当前head的waitStatus状态改为向下传播
        时刻4: t1获取信号量成功,调用setHeadAndPropagate时(执行setHead之前,head的值还是时刻1的head),因为不满足propagate > 0(时刻2的返回值也就是propagate==0),从而不会唤醒后继节点,
        但满足 h.waitStatus < 0的条件,因此会唤醒t2线程

        也就是说,上面会产生线程hang住bug的case在引入PROPAGATE后可以被规避掉。在PROPAGATE引入之前,之所以可能会出现线程hang住的情况,就是在于
        releaseShared有竞争的情况下,可能会有队列中处于等待状态的节点因为第一个线程完成释放唤醒,第二个线程获取到锁,但还没设置好head,又有新线程释放锁,但是读到老的head状态为0导致释放但不唤醒,最终后一个等待线程既没有被释放线程唤醒,也没有被持锁线程唤醒。
         */
        for (;;) {
            // head -> A
            Node h = head; // 从head节点开始唤醒,head节点就代表当前时刻获取到锁的节点
            if (h != null && h != tail) { // 至少两个节点
                int ws = h.waitStatus; // head节点的状态
                if (ws == Node.SIGNAL) { // 如果为signal,负责唤醒后继节点
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 把head waitStatus置成初始状态
                        continue;            // loop to recheck cases
                    unparkSuccessor(h); // 唤醒后继线程
                }
                /*
                    如果ws为0,说明多个释放锁的线程拿到的head是同一个,为保证队列的活跃性把当前head指向的节点 waitStatus置为PROPAGATE
                    这样其它线程能够继续传播,唤醒节点
                 */
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // head -> B
            if (h == head)// loop if head changed  如果head没有改变退出循环,否则一直循环
                break;
        }
    }

简单总结一下共享锁获取流程:
1: 尝试获取锁,成功返回,失败调用doAcquireShared加入等待队列,等待被唤醒 2:被唤醒之后调用setHeadAndPropagate,如果还有剩余资源,还要负责唤醒后继节点

参考:www.cnblogs.com/waterystone…