AQS

215 阅读2分钟

AQS是j.u.c包下的一个框架,被用来实现各种锁以及其他的同步框架,比如ReentrantLock,Semophare,CyclicBarrier,CountDownLatch等。

AQS的数据结构(无序列表乱码了,编辑器内正常)

  • CLH队列

  • AQS中是通过一个CLH队列来实现线程的阻塞,唤醒的。

  • 该队列是一个双向链表,每个节点代表了一个线程,每个节点都有prev和next两个指针。

  • 节点还有一个waitStatus的int类型的状态,用来表示该线程当前的状态。包含

  • cancelled

  • signal

  • condition

  • propagate

  • 新来的线程如果acquire失败,会包装成一个新的Node放到链表尾部(尾插法)

  • 之前的线程如果release成功,会唤醒下一个线程,也即head的下一个节点。head本身是一个虚节点。

  • state

  • 状态属性,int类型,volitile修饰。用来表示资源

  • 举例

  • ReentrantLock

  • 0 代表锁没有被任何线程持有

  • 大于0 代表锁被某线程持有,具体数值代表持有次数

  • CountDownLatch

  • countDown方法会将state减一

  • await会阻塞直到state减到0

ReentrantLock

下边我们结合ReentrantLock来具体分析下AQS的相关源码

lock的执行过程

1.lock方法实际调用的是acquire方法,acquire代码如下:

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

首先tryAcquire一次,如果成功了,代表获取锁成功;如果失败,则包装成一个Node尾插到CLH队列尾部,同时对该Node进行acquireQueued操作(下边会介绍acquireQueued方法)。

2.tryAcquire方法是我们需要实现的逻辑:

以非公平锁为例:

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            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;
        }

首先我们判断当前锁是否被持有,如果没有,通过CAS的方式设置成持有状态,并将本线程设置为持有线程,返回true,代表lock成功;如果当前锁已经被持有且是被当前线程持有,则增加state,代表重入+1,返回true,代表lock成功;否则代表当前锁被其他线程持有,返回false,代表lock失败。

3.acquireQueued

    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)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

for循环内,如果node的前驱节点是头节点,即node是第一个非虚节点(head是虚节点),当前线程(也即node的线程)才有资格去继续执行tryAcquire的逻辑。如果acquire成功,会将该节点设置为head,并将该节点的thread,prev设置为null,使其也成为一个虚节点,保持head虚节点的特性。

如果node的前驱节点不是head,满足条件时会将当前线程(也即node的线程挂起)。

到此,整个lock(acquire)的过程结束。

简单来讲,首先尝试一次,如果成功返回;如果失败会放入队列尾部,继续尝试,但其实只有当前node成为第一个非虚节点才真正有资格进行tryAcquire的操作。这个地方会挂起避免空转。当持有线程释放时,会唤醒后边的节点(下边释放逻辑会讲到)。

unlock的执行逻辑

1.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;
    }

        protected final boolean tryRelease(int releases) {
            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;
        }

由于ReentrantLock是可重入的,因此tryRelease如果只是减少了重入次数,不代表真正释放资源,会返回false;只有完全释放资源,也即state变为0,才会返回true。

release方法中,当tryRelease返回true,即当前线程(也即调用unlock的线程,也即获取到锁资源的线程)真正释放资源时,才会去唤醒head的后继节点的线程(unparkSuccessor(head))。而被唤醒的线程会去执行前边分析的acquireQueued逻辑,尝试获取资源(如果是第一个非虚节点的话)。

由此及acquireQueued中的tryAcquire方法可知,CLH队列管理的线程获取资源是按照先来后到的,是公平的。