终于把AQS原理给讲明白了

337 阅读10分钟

这几天刚好在看并发相关内容,谈到并发就离不开锁,那我们就来聊聊AQS吧。 AQS是AbstractQueuedSynchronizer的简称,顾名思义,抽象队列式同步器,FIFO结构的一个队列

先讲明白AQS相关的概念:

1.AQS的概念?上面说过了,他是AbstractQueuedSynchronizer的简称,是由Doug Lea完成编写的(完成JUC的大佬,java.util.concurrent包,据说他的每一行代码都是经典,确实如此)。

2.AQS帮我们完成了什么事?他能让我们专注于资源的获取和释放的逻辑,他本身则构建了一个双向队列来实现线程对资源获取的排队工作。好了,这是他的作用,后面我们慢慢细聊。

3.AQS有几种模式?独占式(只能有一个线程获取锁)以及共享式(可以有多个线程获取锁然后并发执行,只有资源数足够)

4.他的实现有哪些?像Mutex,ReentrantLock(独占锁),ReentrantReadWriteLock(共享锁)等等,都是基于AQS来实现的,ReentrantLock我们比较常用,他是可重入(获取锁的线程可再次获取)的排它锁(只能有一个获取锁)

5.他和Synchronized帮我们实现的锁有什么区别?一个是自己实现的一个是JVM帮我们实现的,功能嘛非常明显,自己定制实现的功能肯定多一些啦。但是JVM是一个monitorenter和monitorexit自动获取和释放锁,而基于AQS实现的就需要我们手动获取和释放了。

源码分析

说了上面几个点,也看不出来AQS是个啥东东,我们还是慢慢上代码来细细体会,学习得静下心来,方能事半功倍。

字段

private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

AQS很重要的就是这三个字段了,他分别是AQS维护的队列的头和尾。而这个int类型的state则是资源,提供了三个方法来对state进行修改。可以注意到他们都是使用了volatile关键字进行修饰的,所以是保证了线程的可见性,A的修改,其他线程都能感知到,并重新从内存中进行读取。

protected final int getState() 

protected final void setState(int newState) 

protected final boolean compareAndSetState(int expect, int update)//使用cas对state进行修改

里面还有一个Node类,他是AQS维护的队列中的节点

static final class Node{

    static final Node SHARED = new Node();//独占式锁节点
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;//共享式锁节点
    
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
}

里面有几个值先混个眼熟,后续还会讲到

名称具体含义
CANCELLED表示当前节点已经取消被AQS调度的机会,中断和timeout情况下会触发此状态
SIGNAL表示后继结点在等待自己将他唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL
CONDITION当前节点现在在Condition等待队列上,当有别的线程调用了Condition.signal方法后,他会从等待队列移动到同步队列,等待被AQS调度获取锁执行
PROPAGATE这个是在共享模式下的状态,前驱节点会在执行足够的情况下唤醒后面的n的节点,直到资源不足
DEFALUT初始状态,隐式赋值,表示新节点入队时的状态

然后下面就开始介绍独占锁和共享锁的具体实现

独占式锁实现原理

获取资源

AQS提供了一个顶层的入口框架

//这是独占锁模式的顶层入口
public final void acquire(int arg) {
    /**tryAcquire是获取资源,成功返回true,否则false
    ***addWaiter是将线程加入到队列尾部
    ***acquireQueued是让线程阻塞在队列中,并且自旋获取资源,获取到了才会返回,
    返回值true是中断过,false是没有中断,
    */
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • tryAcquire:尝试获取资源state。
  • addWaiter:如果没有获取到资源,则将线程封装为一个独占式Node,放入同步队列尾部,并将tail设置为这个节点。
  • acquireQueued:放入同步队列后自旋检查自己的前驱节点是不是head了,即当前位置是第二个Node,那么表示自己可以去执行自己的事了(FIFO先进先出结构,AQS总是调度等待时间最长的,即最先入队的),如果不是第二,那就乖乖的LockSupport.park()去吧(阻塞)。

tryAcquire

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

怎么抛异常啊?之前说过AQS只是管理获取资源线程的入队维护工作,而获取资源和释放资源是用户按照自己逻辑定义的。

addWaiter

private Node addWaiter(Node mode) {

    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {//尝试快速添加,因为enq会自旋,可能比这个慢
        node.prev = pred;
        //使用cas的将tail尾结点设为当前的node节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;//返回新添加的node节点
}

如果快速添加失败,这个enq是干啥的啊?来看一下

private Node enq(final Node node) {
    for (;;) {//注意这里是自旋,一定会将node节点给放在尾结点
        Node t = tail;
        if (t == null) { // Must initialize,如果tail为空也就是当前队列没有节点,会创建一个哨兵节点,然后继续构建链表
            if (compareAndSetHead(new Node()))
                tail = head;//只是创建了个哨兵节点,自旋后走下面的else
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {//cas的将尾结点设置成当前node节点
                t.next = node;
                return t;
            }
        }
    }
}

这里挺妙的,自旋的将node添加进尾部。代码也确实十分优美,这就是Dong Lea吗?不愧是大哥李。

acquireQueued

好了这里我们已经将获取不了资源的节点加入到尾部节点了,接下来怎么办啊?得不到就睡觉去呗,该躺平了,就像我们去银行取钱,工作台只有一个,我们拿了个号(排队号addWaiter),然后就可以去沙发上等着前一个人叫我们(哈哈哈,虽然现实中他可能直接溜了)。

final boolean acquireQueued(final Node node, int arg) {//返回是否中断过
    boolean failed = true;
    try {
        boolean interrupted = false;//检测是否被中断过
        for (;;) {//自旋
            final Node p = node.predecessor();//获得node的前驱节点
            //当排到老二的位置了,就能去尝试获取资源了,又是我们自定义逻辑
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //确定自己不是老二,应该寻找一个安全点(自旋的寻找)然后park了。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);//取消资源获取,将node的状态设为cacell
    }

那这个shouldParkAfterFailedAcquire干了啥呢?寻找一个安全点

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//node的前驱节点
    if (ws == Node.SIGNAL)//"pre记得叫我,我是node",好了现在可以安心的park阻塞了
        return true;
    if (ws > 0) {//若前面的节点是cacelled,就需要寻找安全点,然后被跳过的这些cacell节点会被gc
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {//-3,-2,0,叫他们提醒自己(把前驱设成signal通知自己)
        /*
         * 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.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    //调用它的acquireQueued会自旋的进行设置
    return false;
}

下面就是park睡觉啦,如果是在同步队列中线程调用了interrupt方法,他会记录,但是不进行响应(啥也不干)。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

在最外层调用的acquire,条件都成立,他会执行这个方法,他仅仅是将线程的中断标志设置为true(因为在waiting时调用了interrupt方法会清除中断标志)

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

释放资源release

释放资源简单一些,就是将node移除

这里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;
}
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)
        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;
    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);//唤醒下一个符合条件的节点
}

好了,我们开始有点小疑问了,为什么node你唤醒你的下一个节点是从tail尾巴找过来的,而不是从node.next开始找啊?这不都是一样的吗?答案是不一样的,这个跟之前的代码有联系。 我们找到acquireQueued这个方法,可以看到

final boolean acquireQueued(final Node node, int arg) {//返回是否中断过
    try {
        for (;;) {
            final Node p = node.predecessor()
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC,1号位置
                failed = false;
                return interrupted;
            }
          ...省略
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
private void cancelAcquire(Node node) {
    ...省略
    node.waitStatus = Node.CANCELLED;
    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        ...省略
        node.next = node; // help GC,2号位置
    }

为啥从后找,就跟1,2号位置有关啦

1号位置是头节点,p.next=null,毋庸置疑help gc,2号位置node.next = node?这为啥help gc,他是将自己断开,成为一个孤立节点,但是你可能会问,他的pre还在啊。是的,确实还在,但是并不影响GC,逻辑上已经可以回收了,就像你1->2->3->4你想删除2后面的节点,你只需要2.next=null即可。现在如果从前往后找,那么到这个节点的时候next链是断开的,我找不着下一个了,所以你需要从后往前寻找安全一些,画了个图示。

这是一种可能的情况,并不会每次都发生。但是从后往前会安全一些。

图片.png

总结

现在独占锁大概讲明白了,获取不到资源addWaiter然后acquireQueued(自旋)。释放资源unparkSuccesor寻找下一个符合条件的Node。每次只能选择一个Node。

共享锁实现原理

有了前面独占锁的知识,学习共享锁快的多。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

获取共享资源

doAcquireShared

这个是共享锁的顶层入口,可以看到这个tryAcquireShared也是我们自己控制,但是语义规定好了,>=0表示资源足够,<0表示资源不足,需要入队

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);//进入等待队列,节点类型为共享式
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {//自旋
            final Node p = node.predecessor();//前驱节点
            if (p == head) {//当前node是老二
                int r = tryAcquireShared(arg);//尝试获取一下资源
                if (r >= 0) {//资源足够,线程可以做自己的事情的去了,这个老二可以return了
                    setHeadAndPropagate(node, r);//这里不太一样,除了设置新的head,还会在资源足够情况下释放相邻节点。a->b->c。
                    p.next = null; // help GC
                    if (interrupted)//处理一下中断
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            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
    setHead(node);
   
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;//刚才拿到资源的是node,现在node变成head了,接着拿下一个老二
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

可以发现,好像跟acquireQueued差不多啊,只是说中断处理,在函数内处理了,而acquireQueued是在外部处理的。然后就是当某个节点处于老二位置苏醒了,还会唤醒相邻节点,而独占锁是拿到资源的节点release后才来唤醒,共享锁是拿到资源的节点就能直接唤了。确实学习的挺快的,哈哈哈。

释放共享资源

releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {//自己重写,释放资源的逻辑
        doReleaseShared();
        return true;
    }
    return false;
}
private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);//唤醒下一个节点
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

总结

好了,差不多AQS原理也讲完了,这个类里面有2000多行代码,还有一些其他的方法,比如支持响应中断的acquireInterrupt,跟上面的acquire其实差不多。然后现在来总结一下大致的流程。

  • 独占锁:当一个线程申请不到资源时,他就会将直接封装为一个Node节点然后调用addWaiter方法,放入同步队列中,返回返回刚在加入的这个node,再调用acquireQueued方法,不断的进行自旋(如果不是老二他会park),直到他的前一个node,release释放了资源后才会来唤醒当前这个node。
  • 共享锁:同样的,他申请不到资源时(调用tryAcquireShared方法返回值<=0),也会进行资源等待(doAcquireShared方法),如果当前节点被前一个节点唤醒,他会尝试获取资源,一旦能获取成功他就能执行Thread自己的事情,并且将当前node设为Head,并唤醒相邻的节点。