一分钟带你看懂AQS

309 阅读7分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

android中实现锁有两种方式,synchronized和lock,synchronized是基于对象锁机制去实现的,而lock是基于AQS实现的,AQS全称AbstractQueuedSynchronizer,你也可以叫他抽象队列同步器,但AQS并不是lock的专属,比如CountDownLatch内部也是用的AQS。

基本原理

这里是这样的,因为AQS的操作要知道一些原理,才比较好去了解。所以我觉得要先介绍一下这些基本的原理(了解的可以跳过,不了解的需要简单了解一下,这个非常重要)

首先是锁的分类:这里会涉及到公平锁、非公平锁、共享锁、排他锁、乐观锁、自旋锁、可重入锁。

然后AQS里面有几个比较重要的变量。state表示的是加锁的次数,它这又涉及了一个可重入锁的概念,0次表示无锁,>0表示重入锁的次数。Node表示节点,AQS里面是一个双向链表,Node就是链表的节点。exclusiveOwnerThread是持有锁的线程。

AQS的原理(应该说是ReentrantLock非公平锁的原理),简单来说就是竞争资源,竞争失败后生成一个节点加入到队列中(双向链表),等持有资源的线程释放后,队列的第二个结点(也就是头结点的下一个结点)再去竞争资源。

AbstractQueuedSynchronizer解析

我们拿ReentrantLock的代码进行分析。我要先说明一点,AQS里面分为共享锁和排他锁,我们的分析主要是针对排他锁进行分析。

它内部有个内部类Sync继承AbstractQueuedSynchronizer,上面说了,这个锁呢,能分为共享锁和排他锁,但是有过一点基础的都知道ReentrantLock还能实现公平锁和非公平锁。

多啰嗦一句,我给你个结论,Lock能实现公平锁,非公平锁(默认非公平),共享锁,排他锁。再抛出一个问题,公平锁,非公平锁,共享锁,排他锁分别是什么意思?synchronized也能实现这些吗?不懂的朋友可以自己去了解一下,我这就不多BB了,因为这里主要是分析AQS。

这里我是基于29去分析,每个版本源码可能会出现差异,所以别关注源码,主要关注流程。

上面我们说了Sync,ReentrantLock还有两个内部类NonfairSync和FairSync分别表示非公平和公平。lock方法会调用Sync的lock,这里的Sync默认是非公平锁NonfairSync

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

上来就来一个经典的CAS,从概念上来说,你也可以称CAS为乐观锁(不懂这个概念的还是得自己去了解,这里不多说)。成功后调用setExclusiveOwnerThread就是设置当前线程给exclusiveOwnerThread。CAS失败的话调用acquire(1),这是AQS的方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire是抽象方法,由子类去实现,这里就是一个模板模式的体现,我只管流程,不care具体操作,这些具体的操作子类自己去实现。NonfairSync的tryAcquire会调用到Sync的nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 为0就是没加锁
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
    // 判断持有锁的线程和当前是同一个线程走重入锁的操作,state就+1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

这里的操作主要是判断是否已加锁,未加锁的话用CAS去加锁,加锁成功后记录的那个前线程。如果当前已加锁,判判断持有锁的线程和当前线程是不是同一个线程,是的话让state+1,这是一个可重入锁的思想。如何加锁成功则返回true,AQS的acquire方法就不会往下执行,否则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

addWaiter是创建结点的操作

private Node addWaiter(Node mode) {
    Node node = new Node(mode);
    // CAS加死循环,形成一个自旋的操作
    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            U.putObject(node, Node.PREV, oldTail);
            // 通过CAS把结点加到链表尾部
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            // 创建头结点
            initializeSyncQueue();
        }
    }
}

这里会判断链表为空的话,先创建一个头结点,然后再创建一个结点用CAS把结点添加到尾部i。这里要注意,头结点是为了方便引用和判断,所以第一个真实的在阻塞的结点是头结点的下一个结点。

创建完节点后调用acquireQueued方法把刚才创建的节点传进去。

final boolean acquireQueued(final Node node, int arg) {
    try {
        // 中断状态,这个可以先不看
        boolean interrupted = false;
        for (;;) {
            // 拿到前一个结点
            final Node p = node.predecessor();
            // 判断这个结点的前一个结点是不是头结点,并且尝试tryAcquire竞争资源
            if (p == head && tryAcquire(arg)) {
                // 成功后把这个结点设为头结点
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            // 不是头结点或者是头结点竞争资源失败的情况
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } catch (Throwable t) {
        // 异常就取消这个结点
        cancelAcquire(node);
        throw t;
    }
}

这是比较重要的一部步,需要了解我上面说的,头结点是为了方便引用和判断p == head就是为了拿头结点的下一个结点,tryAcquire就是子类做的操作(CAS竞争资源或者判断是否是同个线程可重入锁的情况)。

如果是头结点的下一个结点(注意这里不能称之为第一个等待的结点,因为这个是同步队列,还有一个概念是等待队列,这个倒是可以称为队列中第一个阻塞的结点)。如果是队列中第一个阻塞的结点并且通过tryAcquire竞争到锁,则把这个结点变为头结点(它的下一个结点就变成了队列中第一个阻塞结点)。此时这轮流程就结束,不会继续执行acquire的selfInterrupt方法
否则,调用shouldParkAfterFailedAcquire方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 一般会走这个
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

这个就是设置结点的状态,一般默认状态是0,要设置成SIGNAL,这个是-1。这里有个很有意思的地方,很多人第一次看不注意,只以为这里是用CAS设置状态,其实这里会配合调用它的acquireQueued方法的死循环来形成自旋,自旋到把状态成功设置成阻塞状态为止。 其实这里不难理解,因为我线程的情况下直接设置状态是不安全的,但是我们又期望能把这个状态变为阻塞状态,所以用自旋。

还要注意的是,这里不是只设置队列中第一个阻塞的结点,因为不是头结点的下一个结点的那些结点都会执行进来,所以这里是把除了头结点以外的结点都设为阻塞状态。

当结点成功设置成阻塞状态之后,就会往下调用parkAndCheckInterrupt()

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

它会调研LockSupport.park(this)阻塞线程。这是底层的方法,可以先不用管,我们这里主要是将流程,你只需要先了解调这个方法后线程会阻塞就行。

有阻塞那自然有唤醒,我们来看unlock方法,ReentrantLock的unlock方法会调用到AQS的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同样丢给子类去具体实现

protected final boolean tryRelease(int releases) {
    // 重入锁的数量-1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

可重入锁就是加多少次锁就要解多少次锁。所以这里调一次unlock解一层锁,等state变成0的时候才是真正的释放锁,这时候把AQS记录的线程exclusiveOwnerThread设为null。成功解锁后再执行release之后的逻辑,主要就是调用了unparkSuccessor方法

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

这里的Node就是头结点,把头节点的状态改了,s的next就是第一个阻塞的节点,正常它的waitStatus是阻塞状态会是-1,所以不会进到判断,然后调用LockSupport.unpark(s.thread)其实就是唤醒这个节点。

唤醒之后代码会从阻塞的地方继续执行,从上面我们可以看出,唤醒之后就继续执行acquireQueued方法。这就是AQS的主要流程。

AQS涉及的知识点

其实AQS还有很多细节我这里没讲,主要是讲了ReentrantLock实现加锁解锁时,AQS的操作,还有一些共享锁,中断,等待队列这些,这里都没讲,那些地方也是会细节满满。可以看到他的代码其实不算多,但是却能实现很多的功能。

分析这一块代码主要是我觉得AQS里面有些设计做得太好了,有些操作也是非常的赞,平时的开发也可以去学习这些操作来提高代码的质量。

接下来都是一些个人的主观观点,如果有说的不对的地方,还望各位大佬能够指出。首先就是模板方法,它其实内部只是做了对队列的操作,其它的判断什么的,交给子类去做,这使得AQS的扩展性非常的好,它做的事并不依赖加锁解锁,它说做的从始至终都是维护队列,也就是Node。拿我以前来说,我刚毕业不久的时候,看设计模式,去看别人的Demo,其实看完了就完了,基本都学不到什么,但是看到AQS里面这种操作之后,当时我看了之后实属是有了更深的理解。

然后是CAS和自旋,这里面基本都用到,实属是把这两个东西给玩得明明白白,改变状态, 插入队列,竞争锁,全用了自旋,如果你还不明白CAS,不明白自旋,我真的建议多学学这的操作。

还有就是我提到的锁的种类,公平锁、非公平锁、共享锁、排他锁、乐观锁、自旋锁、可重入锁这些,如果你只理解一个概念,不了解具体怎么实现的,AQS就告诉你了(当然我们这里没有讲公平锁和排他锁,但是AQS的源码里有)

其它的,有些东西确实是只可意会不可言传。我非常建议没看过这块代码的人去看一看,我这里介绍的也仅仅是一部分的东西,你只看我的文章,学不到多少,自己去看看相信你会有自己的理解。

总结

这次主要分析了ReentrantLock的主要实现是通过AQS,AQS内部的主要操作。AQS中值得学习的开发技巧。最后还是很建议自己去思考一遍AQS的源码,绝对收获颇丰。