java.util.concurrent 之 队列同步器AQS

本文详细解析了Java并发包中的核心组件AQS(AbstractQueuedSynchronizer)的工作原理,包括其内部使用的双向链表FIFO队列结构、关键方法如CAS操作等,以及如何通过自旋等待等方式获取和释放锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

现在公共平台一些一直在用的老系统(UM),用户量越来越多,并发量越来越大,对java.util.concurrent 包的使用已经必不可少,很多时候我们只是使用这个包中的一些类,至于这些类为什么能够有这么优美的性质,我们要是不知道,那对于一个Java程序员来说这是比较可惜的,我准备从最基本的AQS(队列同步器)和CAS(compareAndSet操作)开始,一步一步介绍这些类,也是提升自己。今天来看AQS。

AQS(AbstractQueuedSynchronizer),中文叫队列同步器,他是构成concurrent包中各种线程安全数据结构的基础,比如各种阻塞队列,甚至各种锁。锁的话可能有些人比较困惑,锁和队列有什么关系?其实这就像Set和Map的关系,HashSet其实里面维护了一个HashMap,其主要操作都是由这个Map完成的,这个Set中的所有元素都作为这个HashMap的Key,所以有不重复的性质。锁的主要操作也是由其继承AQS来完成的。

那么这个AQS既然名字叫队列同步器,那么它肯定与队列有关,事实上它就是定义了一个基于双向链表的FIFO队列,然后规定了一些参数(是否独占、是否共享、是否在等待Condition等等)、采用了一些关键的方法(CAS操作即原子操作),定义了一些接口。下面是Doug Lea自己画的AQS的图。。。怎么有点萌

<p>To enqueue into a CLH lock, you atomically splice it in as new
 * tail. To dequeue, you just set the head field.
 * <pre>
 *      +------+  prev +-----+       +-----+
 * head |      | <---- |     | <---- |     |  tail
 *      +------+       +-----+       +-----+
 * </pre>

这个里面的一个个的方框就是Node,这个Node的核心属性当然是Thread,AQS实际上描述的就是一个个的Thread在一个队列中排队,排到了哪个线程那个线程就出去,然后唤醒他的下一个节点。然后既然是链表操作,那每个Node自然有前驱节点和后驱节点,即predecessor和successor。

好,这个数据结构我们就了解到这里了,就是一些大学学的链表队列知识,我们先看Doug Lea大神给AQS定义的通用接口:

acquire(int)//独占获取锁

acquireInterruptibly(int)//独占式获取锁,获取锁时可被中断

tryAcquireNanos(int, long)//独占式获取锁,获取锁时可被中断,也可设置超时中断

release(int)//释放独占锁

acquireShared(int)//共享方式获取锁

acquireSharedInterruptibly(int)//共享方式获取锁,获取锁时可被中断

tryAcquireSharedNanos(int, long)//共享方式获取锁,获取锁时可被中断,也可设置超时中断

releaseShared(int)//释放共享锁

hasQueuedThreads()//队列中是否有正在等待的线程

hasContended()//队列是否为空

getFirstQueuedThread()//获取队列中的第一个线程,并选出他的继任节点作为下一个head

isQueued(Thread)//线程是否在队列中

getQueueLength()//获取队列长度

getQueuedThreads()//获取队列中的所有Threads,放在一个List中

getExclusiveQueuedThreads()//获取队列中的,所有尝试获取独占锁的线程

getSharedQueuedThreads()//获取队列中的,所有尝试获取共享锁的线程

toString()//变成字符串

owns(ConditionObject)//检查一个ConditionObject是否是由这个AQS作为他的锁,以后讲到condition再说

hasWaiters(ConditionObject)//这次不讲

getWaitQueueLength(ConditionObject)//这次不讲

getWaitingThreads(ConditionObject)//这次不讲

我们就讲 acquire(int) 、acquireInterruptibly(int)、tryAcquireNanos(int arg, long nanosTimeout)这 3 个方法,其余的自己举一反三(看源码)了。

我们先来看 acquire 方法,源码如下,所有获取锁的方法基本流程都是这样,所以我就不每个acquire都写了,有点类似于Double-check,先去获取锁,获取不到再自旋地获取锁:

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

首先,去尝试获取这个AQS的独占锁,也就是tryAcquire(arg),是需要自己实现的一个方法,这个arg参数就要看具体的实现类了。比如ReentrantLock 这个类就只表示一个锁状态,调用它的lock()方法时,实际上是执行tryAcquire(1)。
然后以执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这个方法主要是以自旋的方式等待获取锁,我们具体来看一下:

final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
    }

首先,要知道,我们是要获取一个队列的锁,如果需要从队列中取出一个线程来操作,那必须是从头部获取,所以这个锁肯定是锁在头部,所以这个线程节点要不断地检查自己的前驱节点是不是头部,如果自己的前驱是头部,那就意味着我是第一个节点,而且我尝试获取锁成功(tryAcquire方法返回true),那就可以返回成功了,不光要返回成功,在这之前我还要指定我的下一个节点是第一个节点,也就是把我后继节点的前驱就别指向我了,你指向头部去,这个过程叫做唤醒下一个节点。然后就是p.next = null 这条帮助虚拟机垃圾回收的语句了。

如果此时我不是头部,我就检查自己是不是被中断,如果被中断了,我先记下:interrupted = true,等到我获取了锁,我把这个状态返回出去。

我们可以看到这个方法有一些特点,就是无论这个线程是否中断,无论执行多久,这个方法都会让一个线程无限等待知道他获取锁,并且这个线程会一直存在在队列中知道他前面的线程都执行完毕。所以如果他返回false,则一定是被中断了的,不会再执行,但是我们过了很久才知道,这个就叫做不响应中断,这种方式好不好也不好说,也许程序员需要知道这个程序的中断情况。但如果我不需要知道呢?

所以这个方法有一个增强版本

/**
     * Acquires in exclusive interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    break;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
        // Arrive here only if interrupted
        cancelAcquire(node);
        throw new InterruptedException();
    }

就加了一个break,break完了取消获取锁,取消锁的过程有点复杂,我们来看看:

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // Getting this before setting waitStatus ensures staleness
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If "active" predecessor found...
            if (pred != head && (pred.waitStatus == Node.SIGNAL || compareAndSetWaitStatus(pred, 0, Node.SIGNAL)) && pred.thread != null) {

                // If successor is active, set predecessor's next link
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

这一大段的代码,目的只有一个:把队列中被cancel的节点全部剔除,当然了,由于是队列,是从后面进去,所以我们只需要去剔除我们之前的被cancel的节点就可以了。
首先判断是否为空节点,如果不是空节点,判断节点的线程是否被cancel,通过node.waitStatus这个属性来判断,这个属性只有三个值:

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;

写的很清楚了,只有在线程被cancel的情况下才可能有 node.waitStatus > 0, 循环向前扫描,发现 node.waitStatus > 0的节点就执行 node.prev = pred = pred.prev 跳过,执行完了循环,后面只需要将自己的前驱节点的后继节点指向自己的后及节点就可以了即compareAndSetNext(pred, predNext, next)这条语句,队列的基本操作。只是这里要注意节点是否为头尾节点,如果是尾巴节点,就删除自己,如果是头结点,那么把自己删除之后还需要唤醒自己的后继节点,即

(pred.waitStatus == Node.SIGNAL || compareAndSetWaitStatus(pred, 0, Node.SIGNAL))

基本上就是多了这个剔除被cancel节点的动作,也就是一个从双向链表队列中剔除一个元素,而且是当线程被中断时立即剔除,这个就叫做响应线程中断。

下面看看带超时相应也带中断响应的:doAcquireNanos(int arg, long nanosTimeout),多了一个参数,代表线程获取锁时等待的时长,代码如下:

 /**
     * Acquires in exclusive timed mode.
     *
     * @param arg the acquire argument
     * @param nanosTimeout max wait time
     * @return {@code true} if acquired
     */
    private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
        long lastTime = System.nanoTime();
        final Node node = addWaiter(Node.EXCLUSIVE);
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return true;
                }
                if (nanosTimeout <= 0) {
                    cancelAcquire(node);
                    return false;
                }
                if (nanosTimeout > spinForTimeoutThreshold &&
                    shouldParkAfterFailedAcquire(p, node))
                    LockSupport.parkNanos(this, nanosTimeout);
                long now = System.nanoTime();
                nanosTimeout -= now - lastTime;
                lastTime = now;
                if (Thread.interrupted())
                    break;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
        // Arrive here only if interrupted
        cancelAcquire(node);
        throw new InterruptedException();
    }

唉,其他的我们都知道了,我们只需要知道这个时间怎么算就可以了:

long now = System.nanoTime();
nanosTimeout -= now - lastTime;
lastTime = now;

这个相当于倒计时,把设置的超时时间减完,就cancel当前的线程节点,并返回false。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值