• Java多线程全源码分析


    Java多线程源码分析之:AQS基础

    Java并发的基石:AQS

    AQS简单介绍

    AQS全称:AbstractQueuedSynchronizer,继承自AbstractOwnableSynchronizer(是同步器的基础),它是一个抽象类,定义了同步器的基本框架,而具体的实现是由子类定义的,例如ReentrantLock、Semaphore、CountDownLatch等。

    另外,AQS还有一个孪生兄弟,同样继承自AbstractOwnableSynchronizer,为AbstractQueuedLongSynchronizer。

    与AQS唯一不同的是在AbstractQueuedLongSynchronizer中同步器状态相关的字段(waitStatus)都被定义为了long类型

    来自源码注释的说法:

    此类具有与AbstractQueuedSynchronizer完全相同的结构、属性和方法,不同之处在于所有与状态相关的参数和结果都定义为long而不是int 。 在创建需要 64 位状态的多级锁和屏障等同步器时,此类可能很有用。

    假定你已经熟悉了LockSupport的基本用法

    先来看看AQS的主要属性有哪些。

    // 头结点
    private transient volatile Node head;
    
    // 尾节点
    private transient volatile Node tail;
    
    // 同步器状态,
    // 0:表示未被占用,
    // >=1:表示加锁次数,就是可重入锁的概念
    private volatile int state;
    
    // 继承自父类:AbstractOwnableSynchronizer
    // 代表当前持有锁的线程
    private transient Thread exclusiveOwnerThread; 
    

    由于AQS本质是一个双向队列,节点为内部的Node类,负责将线程包装起来组成队列。下面看一下Node的数据结构。

    AQS是一个FIFO(先进先出)型的队列,是由CLH队列锁的变种实现

    CLH锁:

    简单来说就是节点中有一个状态值,在AQS中其实就是下面的waitStatus字段,

    然后当前节点和后节点都会对这个状态进行自旋判断,

    对于当前节点来说,一旦释放锁就立即改变waitStatus的值,

    对于后节点来说,一旦waitStatus值改变并达到解锁状态就代表可以获取锁了。

    其实也就四个属性,其中可能Node需要着重解释一下。

    //共享模式
    static final Node SHARED = new Node();
    
    //独占模式
    static final Node EXCLUSIVE = null;
    
    // waitStatus属性相关常量:
    
    //当前节点由于超时或者中断被取消,节点进入这个状态以后将保持不变。
    //注:这是唯一大于0的值,很多判断逻辑会用到这个特征
    static final int CANCELLED =  1;
    
    //后继节点会依据此状态值来判断自己是否应该阻塞,当前节点的线程如果释放了同步状态或者被取消
    //会通知后继节点、后继节点会获取锁并执行。当一个节点的状态为SIGNAL时就意味着其后置节点应该处于阻塞状态
    static final int SIGNAL = -1;
    
    //当前节点正处在等待队列中,在条件达成前不能获取锁。
    static final int CONDITION = -2;
    
    //当前节点获取到锁的信息需要传递给后继节点,共享锁模式使用该值
    static final int PROPAGATE = -3;
    
    //注意:由于是int类型,默认值为0,其实就代表节点初始状态,表示当前节点在sync同步阻塞队列中,等待着获取
    volatile int waitStatus;
    
    //前节点
    volatile Node prev;
    
    //后继节点
    volatile Node next;
    
    //这个就是被Node包装的线程
    volatile Thread thread;
    
    //条件队列专用,后驱节点
    Node nextWaiter;
    

    简单给个AQS的大致图:

    强调一下,head节点是可以看作是哨兵节点,可以看作它不为“阻塞队列”中

    两个基本方法:acquire和release(独占模式下的加锁和释放锁)

    加锁解锁本质其实就是修改AQS中的state值,如下图:

    img

    先来看加锁:

    独占模式可以简单理解为独占锁。

    cas操作,可以理解为原子操作,后面会详细说。

        //以独占模式获取锁
        public final void acquire(int arg) {
            if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
                //结合下面看,这里就是在补偿中断状态
                selfInterrupt();
            }
        }
    

    首先来看tryAcquire()方法,这是具体操作state的方法(也就是抢锁的方法),它给到了子类去实现。

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

    下面是当抢锁失败入队的方法:

    //将当前Node放入阻塞队列的队尾,并返回了当前Node
    private Node addWaiter(Node mode) {
        //将线程封装成一个node节点
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        //为了提升效率,将enq一部分代码提前了。
        //因为队列不为空的情况比较多,且通常来说cas操作还是很容易成功了,
        //所以相对于直接调用enq方法,少了一次方法调用,少了进入循环(enq里面有循环),少了一次判断
        if (pred != null) {
            node.prev = pred;
            //用cas将当前node放入队尾
            if (compareAndSetTail(pred, node)) {
                //将之前的尾节点的后驱指向当前节点(也是当前尾节点)
                pred.next = node;
                return node;
            }
        }
        //如果队列不为空,调用enq方法自旋入队
        enq(node);
        return node;
    }
    
    /**
     * 自旋入队,注意这里方法的是它的前驱节点
     */
    private Node enq(final Node node) {
        for (; ; ) {
            Node t = tail;
            //这里其实就是队列为空的情况
            if (t == null) {
                //用cas初始化一个空的head节点
                if (compareAndSetHead(new Node())){
                    /**
                     * cas创建成功后,由于队列只有一个head节点,所以把AQS的尾节点也指向它。
                     * 主要是防止尾节点为空,且这样让后面的入队更好判断
                     */
                    tail = head;
                }
            }
            //这里是队列不为空的情况,因为随时要考虑到并发,虽然在前面已经判断过了。
            else {
                //将当前节点的前节点指向当前队列的队尾
                node.prev = t;
                //用cas将当前节点放入队尾
                if (compareAndSetTail(t, node)) {
                    /**
                     * 注意这里的t是原来队列的队尾,由于此时已经将当前节点变成队尾了,
                     * 所以将t的下节点指向当前节点形成链表
                     */
                    t.next = node;
                    return t;
                }
            }
        }
    }
    

    下面是acquireQueued()方法:一直自旋,未获取到锁就休眠,直到获取锁。

    //此方法就是让当前节点的线程睡眠
    final boolean acquireQueued(final Node node, int arg) {
        //这里加一个异常信号量其实最重要的是防止非异常情况下后面的finally执行
        boolean failed = true;
        try {
            //这里是定义了一个中断标志来返回
            boolean interrupted = false;
            for (; ; ) {
                final Node p = node.predecessor();
                //aqs秉承了最大限度提升并发,也不忘在休眠前最后也尝试一下让第一个节点抢一下锁
                if (p == head && tryAcquire(arg)) {
                    //这里主要是将当前节点换成头节点,这样做可以最小程度操作链表
                    setHead(node);
                    //注意p是旧的头节点,现在已经没有用了,所以将他的引用去掉
                    p.next = null;
                    //这里把异常的信号量设置为false,防止后面的finally执行
                    failed = false;
                    return interrupted;
                }
                //判断当前节点应该处于什么状态
                if (shouldParkAfterFailedAcquire(p, node) &&
                        //让当前节点休眠
                        parkAndCheckInterrupt()){
                    //中断补偿,执行这里的条件是parkAndCheckInterrupt()里面返回的中断状态是ture,这里
                    interrupted = true;
                }
            }
        } finally {
            /**
             * 根据代码上下文,这里只会在异常情况下执行,
             * 因为上面的循环结束之前会把failed设置为false,
             * 其实正常来说上面的代码其实不会有异常的,可能是一种严谨的操作吧
             */
            if (failed) {
                cancelAcquire(node);
            }
        }
    }
    
    /**
     * 此方法主要是将节点设置为head
     * 这里只会有一个线程进来,只有cas成功的线程才会进来
     */
    private void setHead(Node node) {
        head = node;
        //因为当前的节点已经抢到锁了,所以可以把它的线程应用去掉
        node.thread = null;
        //这里的前驱本来是引用的旧头节点,现在也可以去掉了
        node.prev = null;
    }
    
    /**
     * 此方法用于根据前节点的状态判断当前节点应该怎么做(去拿锁还是休眠)
     * (此方法属于acquireQueued里面的循环调用的)
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果前驱的信号量为-1
        //就代表我可以去安心休眠了
        if (ws == Node.SIGNAL)
            return true;
        //大于零只有一中情况,就是1,代表取消,于是就一直往前面早,直到找到信号量>0的,再把当前节点的前驱指            //向它
        //这里其实也是在处理取消的node
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /**
             * 虽然这里有三种情况:0 -2 -3,我们这里暂时只讨论第一种情况,也就是0初始化
             * 这里用cas将前驱的信号量改为-1
             * 虽然这里有三种情况:0 -2 -3,我们这里暂时只讨论第一种情况,也就是0初始化
             * 在aqs中,我们是通过前驱的信号量来表示当前节点的状态的,因为这里我们的node马上就要去休眠了,
             * 就需要把状态告知前驱,注意此方法默认返回false,当这一次改成-1后,
             * 下一次进入循环目的就是为了严谨的让线程睡眠,因为waitStauts状态有很多种。当然也有可能
             * 我们刚刚把waitStatus改成-1进入第二次循环准备休眠时,直接抢到了锁,休眠失败,当也不影响后面的
             * 线程,因为我进入这个方法本来就是要去休眠了。
             * 我们也可以看到在node抢到锁后将它设置为head后并没有改变waitStatus的值
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    //此方法就是让当前节点休眠。
    private final boolean parkAndCheckInterrupt() {
        /**
         * 这里是正式将自己休眠了,
         * 这里可以体现出中断状态补偿的原因就是
         * 如果不用Thread.interrupted()方法清楚中断状态,这里就无法休眠了
         */
        LockSupport.park(this);
        //注意这个方法会清除中断状态
        return Thread.interrupted();
    }
    

    再来看释放锁,只要加锁理解清楚了,释放锁就很容易懂了。

    首先看看释放锁用调用了那些方法

    image-20220115152216986

    我们可以看到,在释放锁的时候主要调用了tryRelease和unparkSuccessor两个方法。下面来看看代码。

    首先是release()。

    public final boolean release(int arg) {
        //尝试释放锁,失败直接返回false
        if (tryRelease(arg)) {
            Node h = head;
            /**
             * 释放锁成功后,判断头节点不为空且头节点的标志不为零就唤醒阻塞队列中的线程
             * ps:这里判空是为了防止空指针异常(因为按照正常流程来说当需要释放锁的时候阻塞队列的头节点是不可能为空的)
             * 判断节点标志是为了判断阻塞队列还有没有等待的线程
             * 这里又需要注意一点:根据aqs来看,加锁解锁其实就用是cas操作来改变aqs的sate值。
             */
            if (h != null && h.waitStatus != 0) {
                unparkSuccessor(h);
            }
            return true;
        }
        return false;
    }
    
    //这里是真正执行释放锁的操作,我们发现在aqs中同样直接抛出了异常,需要实现类自己实现
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    
    //这里是唤醒阻塞队列中等待的线程
    private void unparkSuccessor(Node node) {
    
        int ws = node.waitStatus;
    
        /**
         * 如果waitStatus小于0,用cas把自己改为0
         * 至于为啥改为零,因为马上我们就会释放后驱节点了
         */
        if (ws < 0) {
            compareAndSetWaitStatus(node, ws, 0);
        }
    
        //这里其实如果后驱节点为空或者被取消,就从后往前遍历,直到找到符合条件的节点
        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);
        }
    }
    

    ps:上面从后往前遍历寻找符合条件的node节点的原因如下:

    根据入队的方法enq来看,当我们要入队时,先是将前驱节点指向尾节点,然后再用cas将当前节点变为尾节点,成功了才旧的尾节点的后驱指针指向当前节点(这时候当前节点就是尾节点了)。

    想象一个场景:我先将入队代码贴来

    假如线程A先进来

        private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                if (t == null) {
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        //这里,线程A已经用cas将线程A的node节点变为尾节点了,然后线程A被os切换上下文了
                        //线程A此时一直停留在这里,还没有执行下面的代码。
                        t.next = node;
                        return t;
                    }
                }
            }
        }
    

    并发时线程B进来,同样执行了enq方法,成功入队,成为队尾。

    那么这个时候对于线程A来说,它还是认为自己时队尾节点,且它没有后驱节点,但对于线程B和其他线程来说,还存在着线程B的node节点。

    其实问题就出在线程A

    t.next = node;
    

    这一行还未执行,导致线程A的node节点的后驱指针为空。

    如果我们选择往后遍历,就会出现后驱为空但其实后面存在节点的情况

    同理,在取消节点方法cancelAcquire()里面,

    AQS的核心:CAS原子操作

    什么是原子操作?原子操作就是不可打断的一个操作或一系列操作,这些操作要嘛全部不执行,要嘛全部执行。

    原子操作是由硬件提供的,包括比较与交换(CAS)拿取并累加(FAA)

    CAS:虽然在不同cpu架构中的具体实现不同,但大致是:当我们希望改变一个共享变量时,我们会对比此共享变量的地址有没有改变,如果改变了就说明已经有其他线程操作了,那我们就操作失败,否则,就操作成功,将修改的变量写入。

    FAA:在高级语言中,一般都会提供一个语法糖:i++,虽然只有一行代码,但其实它并非原子操作,它需要三步才能完成。而cpu提供了将这三步包装成原子操作。

    Java中的原子操作类:Unsafe

    Java中的cas操作是通过Unsafe来实现的,这个类不符合jdk的规范,底层应该是C代码再调用的os的api。

    具体其实没必要了解那么多,可以简单理解为这是一个通过C代码来为Java提供操控内存的能力。

        public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    
        public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
        public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
    

    在aqs中其实主要就用到了这三个方法,分别是:cas修改对象、cas修改int值、cas修改long值。

    AQS中Condition条件队列

    我们可以看到,在AQS中有一个内部类:ConditionObject,它实现了Condition接口。它其实就是我们所说的条件队列。

    首先来看一下Condition这个接口都有那些方法。

    //等待并响应中断
    void await() throws InterruptedException;
    
    //等待并不响应中断
    void awaitUninterruptibly();
    
    //按时间等待并相应中断
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    
    //按时间等待并相应中断,有超时时间
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    
    //按照日期等待并相应中断
    boolean awaitUntil(Date deadline) throws InterruptedException;
    
    //唤醒一个线程
    void signal();
    
    //唤醒所有线程
    void signalAll();
    

    可以看到,条件队列其实也是只有两个核心方法:等待和唤醒

    注意:我们现在一直还是停留在AQS源码中,它只是个抽象类,很多方法都还不是很完整。

    这里的条件队列其实主要是由AQS的实现类之一:ReentrantLock进一步实现的,并且所有的等待和唤醒操作必须要先获取独占锁才能执行。

    再介绍什么是条件队列时我们先回顾一下AQS中的Nde节点有哪些属性。

    //共享模式
    static final Node SHARED = new Node();
    
    //独占模式
    static final Node EXCLUSIVE = null;
    
    /**
     * 当前节点由于超时或者中断被取消,节点进入这个状态以后将保持不变。
     * 注:这是唯一大于0的值,很多判断逻辑会用到这个特征
     */
    static final int CANCELLED =  1;
    
    /**
     * 后继节点会依据此状态值来判断自己是否应该阻塞,当前节点的线程如果释放了同步状态或者被取消
     * 会通知后继节点、后继节点会获取锁并执行。当一个节点的状态为SIGNAL时就意味着其后置节点应该处于阻塞状态
     */
    static final int SIGNAL = -1;
    
    // 当前节点正处在等待队列中,在条件达成前不能获取锁。
    static final int CONDITION = -2;
    
    // 当前节点获取到锁的信息需要传递给后继节点,共享锁模式使用该值
    static final int PROPAGATE = -3;
    
    // 注意:由于是int类型,默认值为0,其实就代表节点初始状态,表示当前节点在sync同步阻塞队列中,等待着获取
    volatile int waitStatus;
    
    // 前节点
    volatile Node prev;
    
    // 后继节点
    volatile Node next;
    
    // 这个就是被Node包装的线程
    volatile Thread thread;
    
    //条件队列专用,相当于上面的next
    Node nextWaiter;
    

    再看一下条件队列有那些属性。

    //条件队列的头节点
    private transient Node firstWaiter;
    
    //条件队列的尾节节点
    private transient Node lastWaiter;
    

    我们可以看到基本只有两个值得关注的,分别是头节点和尾节节点。

    条件队列是一个单向列表,以nextWaiter作为指针相连。它与阻塞队列的关系如下图:

    condition-2

    条件队列的休眠和唤醒操作

    可以看到,条件队列其实就是阻塞队列的一部分,在一定条件下队列的头节点可以入队阻塞队列。

    还是先来看看休眠方法的调用链

    image-20220117111956052

    我们可以看到,它的调用链还是比较多的,值得注意的是,acquireQueued()方法是上面阻塞队列中使用过的,所以这里就不讲了。

    首先来看休眠的代码:

    public final void await() throws InterruptedException {
            //响应中断
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
            //入条件队列
            Node node = addConditionWaiter();
            //完全释放锁,因为锁是可重复的
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            /**
             * 下面就是自旋判断自己是否在阻塞队列中(强调一下是在阻塞队列中,看上面的图)
             * 为什么要一直判断呢,先提前剧透一下,因为当其他节点把我们唤醒前会将我们移动至阻塞队列,
             * 且这里会存在虚假唤醒:因为我们唤醒后
             */
            while (!isOnSyncQueue(node)) {
    
                LockSupport.park(this);
                /**
                 * 被人唤醒后,判断自己是否发生了中断,没有发生中断值为0
                 * 没发生中断就代表正常流程,自旋判断是否有已经入队
                 */
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
                    break;
                }
            }
            /**
             * 进入阻塞队列后:
             * 见到一个很熟悉的方法“acquireQueued()“,这个方法前面说过,就是让当前节点休眠等待被唤醒
             * 然后判断了一下中断类型
             * 为什么要判断中断类型呢,别忘了我们的”await()方法是要响应中断的“
             */
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
                interruptMode = REINTERRUPT;
            }
            /**
             * 处理一下取消的node
             */
            if (node.nextWaiter != null) {
                unlinkCancelledWaiters();
            }
            //响应中断(不为零就代表睡眠时被中断了)
            if (interruptMode != 0) {
                reportInterruptAfterWait(interruptMode);
            }
        }
    

    下面是入队的方法。

    private Node addConditionWaiter() {
        //获取尾节点
        Node t = lastWaiter;
        //队尾不为空且队尾的waitStatus为非条件队列的专用状态就是无效的节点(取消的节点)
        if (t != null && t.waitStatus != Node.CONDITION) {
            //清除队列的所有无效节点
            unlinkCancelledWaiters();
            //因为清除了无效节点后队尾改变了,重新再引用一下
            t = lastWaiter;
        }
        //将当前线程包装成node节点(是不是与阻塞节点类似,这里也进一步说明了其实条件队列就是aqs的一个套娃队列)
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        //这里其实就代表队列一个元素没有了,所有当前节点就顺理成章为头节点了
        if (t == null) {
            firstWaiter = node;
        } else
            //当尾节点不为空,就将尾节点的后驱指向当前节点
        {
            t.nextWaiter = node;
        }
        //最后将尾节点替换成当前节点
        lastWaiter = node;
        return node;
    }
    

    我们可以看到条件队列的入队还是相对简单的,因为不存在并发,且是单向链表。

    且它和阻塞队列都一样,在队列为空时都会将头节点和尾节点指向当前入队的节点。

    下面是清除条件队列中已经取消的节点

    /**
     * 下面其实就是一个单项链表操作
     * 首先我们需要注意:
     * trail代表的是上一个离当前指针最近的一个有效节点(有点绕,尽量读慢点)
     * t为当前循环的节点
     * next为t的后驱
     */
    private void unlinkCancelledWaiters() {
        Node t = firstWaiter;
        Node trail = null;
        //从头节点开始往后遍历
        while (t != null) {
            Node next = t.nextWaiter;
            //当当前指针的waitStatus是取消状态时
            if (t.waitStatus != Node.CONDITION) {
                //为了方便gc将它的后驱指向null
                t.nextWaiter = null;
                
                /**
                 * 下面三个判断有点绕,我一下说了
                 * 首先我们来看什么时候trail为null?只有在头节点是取消状态才会
                 * 如果trail不为空,继续往下走,直接让trail的后驱指向当前节点的后驱,
                 * 相当于在链表中删除了当前节点。最后其实就是判断了后面还有没有节点,
                 * 没有的话trail其实就是整个队列中的尾节节点
                 */
                if (trail == null) {
                    firstWaiter = next;
                } else {
                    trail.nextWaiter = next;
                }
                if (next == null) {
                    lastWaiter = trail;
                }
            }
            else {
                trail = t;
            }
            t = next;
        }
    }
    
    //完全释放独占锁
    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            //下面release方法在上面有说到,就是cas改变aqs的state的值(不要忘了state其实就是加锁的次数)
            if (release(savedState)) {
                //这里的failed作用其实和上面说的差不多,就是来防止非异常下执行了finally代码
                failed = false;
                return savedState;
            } else {
    
                /**
                 * 再次强调,正常来说这里肯定是可以释放锁成功的,因为持有锁就是当前节点
                 * 如果失败了,肯定是未持有锁咯,说实话我还真想不到什么时候cas失败
                 */
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed) {
                node.waitStatus = Node.CANCELLED;
            }
        }
    }
    

    这个方法是判断线程如果发生过中断,0代表没有中断,THROW_IE代表被唤醒前被中断,REINTERRUPT代表唤醒后中断。

            private int checkInterruptWhileWaiting(Node node) {
                //注意这里的方法是返回中断状态并清除中断状态
                return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
            }
    

    这个方法是判断中断类型(唤醒前还是唤醒后)

    final boolean transferForSignal(Node node) {
        ReentrantLock
        /*
         * 进入阻塞队列需要将状态设为初始化0,如果无法设置,说明即将被唤醒的节点
         * 状态为取消了。
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            return false;
        }
    
        //注意这里入队后返回的是node的前驱
        Node p = enq(node);
        int ws = p.waitStatus;
        /**
         * 如果ws>0代表取消了,我们就可以唤醒它让它自己清除掉自己
         * 如果ws<=0,这里用到了cas将ws变为-1,很熟悉的操作,阻塞队列入队后需要将前驱节点改为-1
         * 如果cas失败我们同样唤醒线程来清除掉被取消的前驱,
         * 其实if里面为ture就代表前驱被取消了,我们唤醒线程目的就是为了清除它
         */
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
            LockSupport.unpark(node.thread);
        }
        return true;
    }
    

    不要忘了最后的处理中断的方法

    /**
     * 处理中断,这里只为在唤醒前被中断的情况抛出了异常
     * 唤醒后被中断这种情况进行呢中断补偿
     * (可以看到这里的中断补偿机制与上面的阻塞队列思想一致)
     */
    private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
        if (interruptMode == THROW_IE) {
            throw new InterruptedException();
        } else if (interruptMode == REINTERRUPT) {
            selfInterrupt();
        }
    }
    

    我们可以看到,即使被中断了,我们还是会进入阻塞队列。

    关于中断时机,一共有三种:

    1. 未中断:这也是正常的情况下。
    2. 在被唤醒之前中断。
    3. 在被唤醒之后中断。

    并且可以看到Doug Lea在设计响应中断的思路是,不管中断与否,我们先按照正常流程走一遍,最后再判断中断状态

    其实也可以在“自旋判断是否进入呢阻塞队列”里面就处理中断

    唤醒是啥?别急,我们马上就要说到,可以结合上下文一起看

    下面来看看唤醒的代码,还是老样子,先看看唤醒操作的调用链:

    image-20220117204700191

    我们可以看到,其实唤醒还是比较简单的:主要流程就是将头节点也就是当前节点放入阻塞队列中,并唤醒后驱节点。

    下面来看代码。

    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }
    

    首先是当前线程是否持有锁,这个直接抛出异常,由实现类实现。

    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }
    

    唤醒的逻辑

        private void doSignal(Node first) {
            do {
                /**
                 * 这里有点绕,大概就是如果后驱为空,我们就把尾节点设为空,因为后驱为空代表队列没有了
                 * 注意这里将条件队列的头节点设置为了头节点的后驱
                 */
                if ( (firstWaiter = first.nextWaiter) == null) {
                    lastWaiter = null;
                }
                //这里的意思是因为马上我们头节点要入队了,后驱指针没有用了
                first.nextWaiter = null;
            }
            /**
             * 由于上面将firstWaiter设置为了后驱,这里其实就是当头节点转移失败,
             * 那么选择后面的第一个节点进行转移,依此类推
             */
            while (!transferForSignal(first) &&
                    (first = firstWaiter) != null);
        }
    

    从条件队列转移到阻塞队列的逻辑

    final boolean transferForSignal(Node node) {
        /*
         * 进入阻塞队列需要将状态设为初始化0,如果无法设置,说明即将被唤醒的节点
         * 状态为取消了。
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            return false;
        }
    
        //注意这里入队后返回的是node的前驱
        Node p = enq(node);
        int ws = p.waitStatus;
        /**
         * 如果ws>0代表取消了,我们就可以唤醒它让它自己清除掉自己
         * 如果ws<=0,这里用到了cas将ws变为-1,很熟悉的操作,阻塞队列入队后需要将前驱节点改为-1
         * 如果cas失败我们同样唤醒线程来清除掉被取消的前驱,
         * 其实if里面为ture就代表前驱被取消了,我们唤醒线程目的就是为了清除它
         */
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
            LockSupport.unpark(node.thread);
        }
        return true;
    }
    

    这里做一下关于aqs的中断相关的知识复习吧:

    aqs的唤醒和休眠是由LockSupportunparkpark来实现的,

    LockSupport.park():可以理解为它要找线程要一个许可证,没有就把线程休眠。(注意是会消耗许可证)

    LockSupport.unpark():给线程发一个许可证,最多一个。

    特殊:如果当前线程为中断状态,那么它相当于拥有了一个永久的许可证。

    我们在aqs可以看到Doug Lea用的判断线程是否为中断的方法都是Thread.interrupted(),这个方法是返回线程的中断状态并且重置中断状态。

    所以我们可以看到Doug Lea用到了中断补偿机制。

    思考一下,为什么要用带有清除中断状态的方法呢?

    答案是由于aqs本身是为了并发而生大的,并且利用自旋使用了LockSupport来控制休眠和唤醒,如果不将线程中断状态重置,会影响LockSupport的控制。

    既然中断会影响LockSupport,那么为什么用Object的wait和notify来实现呢?

    这个问题问得很好。不过篇幅有限,且与synchronized机制有关,我会在后面讲到时再专门说明。

    共享模式下的AQS

    前面都是在说独占模式,下面来说说AQS中的共享模式。

    获取共享锁的方法只有两个子类,其中,用cas操作state的方法跟独占模式一样,都直接抛出了异常,由子类负责实现。

    public final void acquireShared(int arg) {
        //这里小于零就代表获取锁失败,准备进入休眠
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    

    下面是休眠的方法。

    private void doAcquireShared(int arg) {
        /**
        * addWaiter这个方法在前面的独占锁里面说过,
        * 就是入队阻塞队列,这里是以共享模式进入
        */
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                /**
                * 当我们的前驱节点为头节点
                * 就可以去尝试抢锁了
                */
                if (p == head) {
                    /**
                    * 这个是由子类实现的抢锁操作,
                    * 这个r是剩余可用的锁数量,如果>=0代表抢锁成功
                    * <0代表抢锁失败
                    */
                    int r = tryAcquireShared(arg);
                    //这是被唤醒后抢到锁的操作
                    if (r >= 0) {
                        /**
                        * 这个方法有点复杂,大概就是抢锁成功后
                        * 去唤醒后面的节点
                        */
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        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;
        setHead(node);
        /**
        * 这个判断有点复杂,一步一步来看。
        * 1. 首先判断了propagate>0,propagate其实就是获取锁后
        * 	还剩余的锁数量,大于零代表还有锁,可以直接唤醒后面的节点
        * h
        */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            /**
            * 下面就是唤醒后驱节点了
            */
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
    
  • 相关阅读:
    mysql的常用查询创建命令
    maven的简介
    google guava
    分库分表的情况下生成全局唯一的ID
    书单
    MD5Util
    UUID生成工具
    nodejs学习笔记三——nodejs使用富文本插件ueditor
    nodejs学习笔记二——链接mongodb
    mongodb 安装
  • 原文地址:https://www.cnblogs.com/lovelylm/p/15750860.html
Copyright © 2020-2023  润新知