JUC学习笔记 - 04AQS源码解析

写在前面: 本文源码基于jdk11来举例,AQS在jdk8和jdk9有了一些变化,但是不影响学习核心源码和设计思想。

首先看一下AQS即AbstractQueuedSynchronizer的类图:

AQS类图.png

可以看到之前整理的大部分锁都是基于AQS实现的。

AQS核心

首先看AQS的属性:

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;
复制代码

这里的state所代表的意思是随子类来定义的,以ReentrantLock为例,0表示没有被占用,当有线程持有该锁则变为1。由于ReentrantLock是可重入锁,所以每次重入时state还可以继续加1。

AQS中维护着一个双向的阻塞队列,如下图所示:

CLH队列.png

AQS维护的是一个名为CLH(Craig, Landin, and Hagersten)队列。再回过头看headtailhead表示头节点。源码中有这样一句注释:To enqueue into a CLH lock, you atomically splice it in as new tail. To dequeue, you just set the head field.,意思是如果要排队进入CLH队列则需要原子地设置为tail,如果要退出队列则只要设置为head即可,可见CLH队列并不包含head节点。

AQS源码

我们以ReentrantLock非公平锁为例来了解源码:

public class TestReentrantLock {
    
    private static ReentrantLock lock = new ReentrantLock();

    void test() {
        try {
            lock.lock();
            // 业务代码
        } finally {
            lock.unlock();
        }
    }
}
复制代码

进入lock方法:

    public void lock() {
        sync.acquire(1);
    }
复制代码

再跟进到sync.acquire(1)

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

跟进到tryAcquire(arg)里又调用了nonfairTrytAcquire(acquires)

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
复制代码
    final boolean nonfairTryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 拿到state
        int c = getState();
        if (c == 0) { // 说明没上锁
            // 给当前线程上锁
            if (compareAndSetState(0, acquires)) {
                // 设置当前线程为独一无二拥有这把锁的线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 判断当前线程是否拥有这把锁
            // 重入
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
复制代码

如果tryAcquire(arg)返回true则表示没有线程在等待锁或重入锁,后续操作就不用再做了。如果没拿到锁,则需要调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。从名字上来猜一下,这里是用排他的形式将当前线程扔到阻塞队列里去。具体先看一下addWaiter(Node.EXCLUSIVE)方法:

    private Node addWaiter(Node mode) {
        // 获取当前要加进来的线程节点
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) { // 尾部节点存在则把新节点放在末端
                // 从名字上看,新节点的prev节点设置为老的尾节点
                node.setPrevRelaxed(oldTail);
                // 用CAS操作把新节点设置为tail
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }
复制代码

源码读到这儿可以稍微总结一下,AQS核心就是用CAS去操作headtail。设想一下如果用锁的话,如果要操作headtail需要锁定整个链表。用CAS只需要关注tail这一个节点就可以了,这也是为什么AQS效率很高。

为什么是双向列表呢?因为添加一个线程节点时需要看一下前置节点的状态,如果前置节点是在持有中,那么新节点就需要在后面等着。如果前置节点已经取消了,那新节点就要越过该节点。整个过程需要查看前置节点的状态,所以必须是双向的。

返回节点后接着调用acquireQueued方法:

    // 此时参数中的node已经进入阻塞队列
    final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            for (;;) {
                final Node p = node.predecessor();
                // p为head说明node是阻塞队列中的第一个节点
                // node试着枪锁
                if (p == head && tryAcquire(arg)) {
                    // node枪锁成功,设置node为head
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                // node不是阻塞队列第一个节点或抢锁失败
                if (shouldParkAfterFailedAcquire(p, node))
                    // 挂起当前线程等待被唤醒
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }
复制代码

接着看shouldParkAfterFailedAcquire方法:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) // 前驱节点状态正常,当前节点需要挂起
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            // 前驱节点取消排队,让当前节点的prev指向正常的前驱节点,好让它来唤醒自己
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 此时状态只可能是0、-2和-3,但前面的源码都没有设置过waitStatus,所以此时状态应该是和刚设置成tail一样,即0
            // 用CAS将前驱节点waitStatus设置为-1
            /*
             * 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.
             */
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }
复制代码

// TODO unlock过程

VarHandle

addWaiter方法中的node.setPrevRelaxed(oldTail)意思是把当前节点的前置节点设置成老的tail

    private static final VarHandle PREV;

    final void setPrevRelaxed(Node p) {
        PREV.set(this, p);
    }
复制代码

假如有这么段代码Object o = new Object(),即o指向了了内存中的new Object()VarHandle指的就是这个引用。但是已经有了对象o了,为什么还要VarHandle呢?

public class TestVarHandle {

    int x = 8;

    private static VarHandle handle;

    static {
        try {
            handle = MethodHandles.lookup().findVarHandle(TestVarHandle.class, "x", int.class);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TestVarHandle t = new TestVarHandle();

        System.out.println((int)handle.get(t));
        handle.set(t,9);
        System.out.println(t.x);

        handle.compareAndSet(t, 9, 10);
        System.out.println(t.x);

        handle.getAndAdd(t, 10);
        System.out.println(t.x);
    }
}
复制代码

上述代码对VarHandle举了几个简单的例子,可以看到它可以操作类的成员变量以及提供一些原子的操作。其实VarHandle的出现主要是替换UnSafe类,UnSafe对于普通程序员来说用起来太危险了。VarHandle也可以代替反射,可以比反射效率高出不少。

猜你喜欢

转载自juejin.im/post/7067804799294668831