IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发工具 -> ReentrantLock核心源码分析,AQS独占模式,可重入锁 -> 正文阅读

[开发工具]ReentrantLock核心源码分析,AQS独占模式,可重入锁

看前须知

? ? ? ? 由于笔记比较多,看的时候,建议把代码copy到VS code 或者IDEA 中查看(为了可以点击方法进行跳转,方便阅读)。主要看方法上面的注释。本篇文章的图片来源于小刘老师的源码培训班。哔哩哔哩搜素:小刘讲源码。本篇文章仅用于作者本人复习源码知识点。

我的笔记

public class ReentrantLock implements Lock, java.io.Serializable {
    //默认使用非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    //sync 在运行时要么是 FairSync 子类,要么是 NonfairSync 子类
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    //ReentrantLock.lock时,会调用相应sync 子类的 lock方法
    public void lock() {
        sync.lock();//这个是不响应中断的lock
    }
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);//响应中断的lock,这个lock 会用到cancelAcquire方法
    }
    
    public void unlock() {
        sync.release(1);
    }
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();       

    }

    /**
    学习ReentrantLock 前,须知
    本笔记中带有//......的地方是指在ReentrantLock 这个可重入的独占锁中没有用到的代码,就不贴出来了。
    为了方便阅读,我把一些方法移动了位置,注意看方法的注释。它表明了这个方法的所属类。

    ReentrantLock 公平锁模式的lock方法,这个方法是不响应中断的。这里所谓的不响应中断指的是
    线程A被 park后,给它一个中断信号,它醒来后,如果A线程的节点不是head的第一个后继 或 锁还没有被释放,
        A线程会在acquireQueued中经过自旋,又会被park。
    
    公平锁加锁的调用链如下:
    ReentrantLock.lock -> FairSync.lock ->AQS.acquire -> FairSync.tryAcquire        
        -> AQS.addWaiter -> AQS.acquireQueued -> AQS.shouldParkAfterFailedAcquire
        -> AQS.parkAndCheckInterrupt -> 返回AQS.acquire方法中调用selfInterrupt,再次给当前线程一个中断信号
        -> 最后返回 业务层逻辑了。
    先讲解的是FairSync。
    

    AQS的等待者队列(head.next -> tail)【双向队列的节点看 waitStatus、thread】、 exclusiveOwnerThread、AQS.state

    先对在ReentrantLock的公平锁中的AQS等待者队列的基本情况进行介绍:
    这个队列是FIFO的、延迟初始化的双向队列。为什么要双向?因为它有那种操作需求:获取当前节点的前置节点,需要判断当前节点是不是head节点的第一个后继节点。
    因为当前节点如果是head.next节点,才有权利去争夺锁tryAcquire.
        延迟初始化是指第一个线程来的时候,直接获取锁成功,将AQS.state从0改为1,exclusiveOwnerThread设为当前线程。
    这时,没有生成队列的head节点。第二个线程来的时候,在AQS.enq()方法中会为第一个线程补上一个Node节点作为head。而这个Node对象是空的构造方法。
    head的意义是逻辑上代表当前独占锁的线程。head节点的thread 为null。waitStatus为-1.

    队列的入队(关注Node节点的waitStatus、thread、prev、next)
    head节点延迟加载。
    在入队的过程中,当前时刻最后插入的node节点a的waitStatus等于0。
        过一会再插入一个节点b的话,b会将a的waitStatus由0改为Node.SIGNAL(-1)。
        反正最后一个入队的节点.waitStatus等于0

    队列的出队,FIFO。新的head的thread设为null。

    取消排队
    AQS中有个cancelAcquire方法,取消排队的。看下文。

    唤醒的情况有2种:
    1.中断方式的唤醒,看看lock是否响应中断。
    如果响应中断,就向上抛出异常,调用cancelAcquire方法取消排队。
    如果不响应中断,有可能继续被park。
    2.调用ReentrantLock.unlock的方式去唤醒
     */    
    static final class FairSync extends Sync {
        //公平锁入口..
        //不响应中断的加锁..响应中断的 方法为:lockinterrupted。。
        final void lock() {
            acquire(1);
        }
        /**
         * AQS.acquire
         * 这个方法就是为了获取锁。如果获取失败就会入队,park。醒来后再尝试获取。最后看看要不要设置中断
         * 它会调用FairSync.tryAcquire、AQS.addWaiter、AQS.acquireQueued、AQS.selfInterrupt方法。
         * 我们一个一个分析。
         */
        public final void acquire(int arg) {
            //条件一:!tryAcquire 尝试获取锁 获取成功返回true  获取失败 返回false。
            //条件二:2.1:addWaiter 将当前线程封装成node入队
            //       2.2:acquireQueued 挂起当前线程   唤醒后相关的逻辑..
            //      acquireQueued 返回true 表示挂起过程中线程被中断唤醒过..  false 表示未被中断过..
            if (!tryAcquire(arg) &&
                    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                //再次设置中断标记位 true
                //lock方法完了以后,回到业务层面了。如果业务层面的代码是响应中断的话,就会去处理一些中断逻辑
                //如果不响应中断的话,就什么也不会发生。
                selfInterrupt();
        }
        /**
         * tryAcquire方法的返回值意义是
         * 尝试获取锁
         * 抢占成功:返回true  包含重入..
         * 抢占失败:返回false
         * 
         * 大体逻辑是:
         * 1.获取AQS的state
         * 2.如果state等于0,说明当前AQS处于无锁状态,就会去检查
         *      队列中除了head节点,在当前线程前面是否有等待者线程。
         *   2.1.如果没有等待者线程,当前线程就会通过CAS的方式将AQS.state从0改为1,
         *          并将ExclusiveOwnerThread设为当前线程,return true;
         * 
         *   2.2.如果当前线程前面有等待者线程,就返回false,获取锁失败,要入队,被挂起。
         * 3.如果AQS.state不等于0,就会再去判断 当前线程是不是独占锁线程。如果是的话,就会走重入锁的逻辑。return true;
         * 4.如果尝试获取锁失败了。return false;
         *     可能原因:
         *     (1)CAS竞争失败了。
         *     (2)当前线程不是独占锁线程,独占锁线程还没有释放锁。AQS.state大于0
         */
        protected final boolean tryAcquire(int acquires) {
            //current 当前线程
            final Thread current = Thread.currentThread();
            //AQS state 值
            int c = getState();
            //条件成立:c == 0 表示当前AQS处于无锁状态..
            if (c == 0) {
                //条件一:
                //因为fairSync是公平锁,任何时候都需要检查一下 队列中在当前线程之前是否有等待者..
                //hasQueuedPredecessors() 方法返回 true 表示当前线程前面有等待者,当前线程需要入队等待
                //hasQueuedPredecessors() 方法返回 false 表示当前线程前面无等待者,直接尝试获取锁..
                //条件二:compareAndSetState(0, acquires)
                //成功:说明当前线程抢占锁成功
                //失败:说明存在竞争,且当前线程竞争失败..
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    //成功之后需要做什么?
                    //设置当前线程为 独占者 线程。
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //执行到这里,有几种情况?
            //c != 0 大于0 的情况,这种情况就需要检查一下 当前线程是不是 独占锁的线程,因为ReentrantLock是可以重入的.
            //条件成立:说明当前线程就是独占锁线程..
            else if (current == getExclusiveOwnerThread()) {
                //锁重入的逻辑..
                //nextc 更新值..
                int nextc = c + acquires;
                //越界判断,当重入的深度很深时,会导致 nextc < 0 ,int值达到最大之后 再 + 1 ...变负数..
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                //更新的操作
                setState(nextc);
                return true;
            }
            //执行到这里?
            //1.CAS失败  c == 0 时,CAS修改 state 时 未抢过其他线程...
            //2.c > 0 且 ownerThread != currentThread.
            return false;
        }
        /**AQS.hasQueuedPredecessors */
        public final boolean hasQueuedPredecessors() {            
            Node t = tail; 
            Node h = head;
            Node s;
            return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
        }

        /**
         * AQS.addWaiter里面有入队逻辑
         * 最终返回一个封装了当前线程的node,这个node此时是已经入队了的。
         * addWaiter的大体逻辑:
         * 1.利用当前线程封装一个node对象。
         * 2.如果队列中已经有节点了,则快速入队,
         *      2.1.当前节点prev指向tail;
         *      2.2.通过CAS的方式去移动tail引用,tail指向当前节点
         *      2.3.CAS成功后,将老的tail.next指向当前节点。return 封装了当前线程的node对象。
         * 3.如果当前队列为空 或者 CAS竞争入队失败了,就调用AQS.enq方法,进行完整入队操作。
         *      AQS.enq方法会保证封装了当前线程的node节点一定入队。
         */
        private Node addWaiter(Node mode) {
            //Node.EXCLUSIVE
            //构建Node ,把当前线程封装到对象node中了
            Node node = new Node(Thread.currentThread(), mode);
            //快速入队
            //获取队尾节点 保存到pred变量中
            Node pred = tail;
            //条件成立:队列中已经有node了
            if (pred != null) {
                 //当前节点的prev 指向 pred
                 node.prev = pred;
                 //cas成功,说明node入队成功
                 if (compareAndSetTail(pred, node)) {
                     //前置节点指向当前node,完成 双向绑定。
                     pred.next = node;
                     return node;
                 }
            }
            //什么时候会执行到这里呢?
            //1.当前队列是空队列  tail == null
            //2.CAS竞争入队失败..会来到这里..
            //完整入队..
            enq(node);
            return node;

        }
        /**
         * AQS.enq()
         * 返回值:返回当前节点的 前置节点。
         * enq方法保证当前线程封装的node 一定会加入到队列里面        
         * 大体逻辑:
         * 1.如果当前队列是空队列,作为当前持锁线程的 第一个后继线程,需要为当前持锁线程补一个node节点作为head。
         *      因为这个队列是延迟初始化的。补完head节点后,就自旋入队
         *      (当前节点prev指向tail,用CAS来移动tail,tail指向当前节点,老的tail.next指向当前节点)。
         *      如果CAS成功,就说明入队成功,构建好双向连接后,就return 当前节点的 前置节点。如果CAS竞争失败,就继续自旋。
         * 2.如果当前队列不是空队列,就通过CAS的方式,尝试入队。CAS成功,就入队了。失败,则继续自旋,去尝试入队。
         * 一定会入队成功。
         */
        private Node enq(final Node node) {
            //自旋入队,只有当前node入队成功后,才会跳出循环。
            for (;;) {
                Node t = tail;
                //1.当前队列是空队列  tail == null
                //说明当前 锁被占用,且当前线程 有可能是第一个获取锁失败的线程(当前时刻可能存在一批获取锁失败的线程...)
                if (t == null) { // Must initialize
                    //作为当前持锁线程的 第一个 后继线程,需要做什么事?
                    //1.因为当前持锁的线程,它获取锁时,直接tryAcquire成功了,没有向 阻塞队列 中添加任何node,所以作为后继需要为它擦屁股..
                    //2.为自己追加node,在自己前面补一个node节点作为head

                    //CAS成功,说明当前线程 成为head.next节点。
                    //线程需要为当前持锁的线程 创建head。
                    if (compareAndSetHead(new Node()))
                        tail = head;
                    //注意:这里没有return,会继续for。。
                }else {
                    //普通入队方式,只不过在for中,会保证一定入队成功!
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
        /**
         * AQS.acquireQueued方法需要 park当前节点的线程  和 唤醒后的逻辑。竞争资源
         * 返回值的意义:
         * true -> 表示当前线程在挂起的时候,被中断唤醒过
         * false ->表示当前线程在挂起的时候,未被中断过
         * 形参 node -> 已经入队的封装了当前线程的node对象
         * 形参 arg -> 这个arg一般是1 。是当前线程尝试获取锁时,通过CAS的方式修改state时用的,从0改为1。
         * 大体逻辑是
         * 1.开一个for自旋,获取当前节点的前置节点。
         * 2.如果 (前置节点是head && tryAcquire尝试获取锁成功了),就做老head的出队操作
         *      2.1.将自己设为head。node.thread设为null,node.prev设为null。
         *      2.2.把老的head.next设为null,帮他 GC
         *      2.3.返回 interrupted。是否被中断过的布尔标记。
         * 3.如果 当前节点不是head.next节点 || tryAcquire尝试获取锁失败了,就  (head.next被唤醒后才有资格去抢锁tryAcquire)
         *      3.1.调用 shouldParkAfterFailedAcquire方法,判断一下是否需要park 当前线程。
         *          shouldParkAfterFailedAcquire方法要么true,要么false
         *              true -> 表示前置节点的waitStatus为SIGNAL(-1),当前线程需要被挂起。
         *              false -> 有2种情况:
         *                  情况1:前置节点的waitStatus大于0,前置节点处于取消状态。
         *                  这个方法会将处于取消状态的node节点出队,给当前节点找一个 waitStatus小于等于0 的前置节点pred。构建双向连接
         *                  node.prev = pred;  pred.next = node;
         *                  情况2:前置节点的waitStatus等于0。就是插入队尾的情况。
         *                  这个方法会将前置节点的waitStatus设为-1。表示前置节点释放锁之后需要 喊醒我
         *      3.2.如果shouldParkAfterFailedAcquire返回true,就调用LockSupport.park(this); 挂起当前线程
         *      3.3.如果shouldParkAfterFailedAcquire返回false,就自旋。
         * 当前线程被挂起后,
         * 如果unpark 唤醒的,就会做自旋操作。
         * 如果是被其他线程唤醒的,就会将interrupted设为true,再做自旋操作。但公平锁的lock方法是不响应中断的,自旋后,大概率会被再次挂起。
         * 公平锁的lockInterruptibly方法会响应中断。线程被挂起后,中断唤醒,会抛出异常,调用cancelAcquire方法做出队操作。
         */
        final boolean acquireQueued(final Node node, int arg) {
            //true 表示当前线程抢占锁成功,普通情况下【lock】 当前线程早晚会拿到锁..
            //false 表示失败,需要执行出队的逻辑... (回头讲 响应中断的lock方法时再讲。)
            boolean failed = true;
            try {
                //当前线程是否被中断过
                boolean interrupted = false;
                //自旋..
                for (;;) {
                    //什么时候会执行这里?
                    //1.进入for循环时 在线程尚未park前会执行
                    //2.线程park之后 被唤醒后,也会执行这里...
                    //获取当前节点的前置节点..
                    final Node p = node.predecessor();
                    //条件一成立:p == head  说明当前节点为head.next节点,head.next节点在任何时候 都有权利去争夺锁.
                    //条件二:tryAcquire(arg)
                    //成立:说明head对应的线程 已经释放锁了,head.next节点对应的线程,正好获取到锁了..
                    //不成立:说明head对应的线程  还没释放锁呢...head.next仍然需要被park。。
                    if (p == head && tryAcquire(arg)) {
                        //拿到锁之后需要做什么?
                        //设置自己为head节点。
                        setHead(node);
                        //将上个线程对应的node的next引用置为null。协助老的head出队..
                        p.next = null; // help GC
                        //当前线程 获取锁 过程中..没有异常
                        failed = false;
                        //返回当前线程的中断标记..
                        return interrupted;
                    }
                    //shouldParkAfterFailedAcquire  这个方法是干嘛的? 当前线程获取锁资源失败后,是否需要挂起呢?
                    //返回值:true -> 当前线程需要 挂起    false -> 不需要..
                    //parkAndCheckInterrupt()  这个方法什么作用? 挂起当前线程,并且唤醒之后 返回 当前线程的 中断标记
                    // (唤醒:1.正常唤醒 其它线程 unpark 2.其它线程给当前挂起的线程 一个中断信号..)
                    if (shouldParkAfterFailedAcquire(p, node) &&
                            parkAndCheckInterrupt())
                        //interrupted == true 表示当前node对应的线程是被 中断信号唤醒的...
                        interrupted = true;
                }
            }finally {//lock()方法调用时,这块逻辑基本用不到。
                if (failed)
                    cancelAcquire(node);
            }
        }
    }
    /**
     * 到这里为止,ReentrantLock的公平锁的加锁过程就完了。
     * 下面说 公平锁的解锁过程。
     * 公平锁解锁的调用链是:
     * ReentrantLock.unlock -> AQS.release -> 
     * sync.tryRelease -> AQS.unparkSuccessor -> AQS.acquireQueued
     * 
     * release的大体逻辑为
     * 1.调用tryRelease方法去释放锁,如果当前线程完全释放锁了,就return true。如果未完全释放锁,则return false。因为有重入锁的逻辑。
     *      如果未完全释放锁,这个方法就直接返回了。
     * 2.如果当前线程完全释放锁了,则会调用unparkSuccessor方法去做唤醒操作。
     *      如果当前节点是最后一个节点了(waitStatus=0),则不用做唤醒操作,直接返回true。
     */
    public final boolean release(int arg) {
        //尝试释放锁,tryRelease 返回true 表示当前线程已经完全释放锁
        //返回false,说明当前线程尚未完全释放锁..
        if (tryRelease(arg)) {
            //head什么情况下会被创建出来?
            //当持锁线程未释放线程时,且持锁期间 有其它线程想要获取锁时,其它线程发现获取不了锁,而且队列是空队列,此时后续线程会为当前持锁中的
            //线程 构建出来一个head节点,然后后续线程  会追加到 head 节点后面。
            Node h = head;
            //条件一:成立,说明队列中的head节点已经初始化过了,ReentrantLock 在使用期间 发生过 多线程竞争了...
            //条件二:条件成立,说明当前head后面一定插入过node节点。
            //h.waitStatus != 0这一步主要是为了 最后一个node节点释放锁的逻辑,
            //   最后一个节点 head、tail都指向它,waitstatus=0,就不用再唤醒后面的节点了。因为后面没有节点嘛
            if (h != null && h.waitStatus != 0)
                //唤醒后继节点..
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    /**
     * Sync.tryRelease() 尝试去释放锁
     * tryRelease的大体逻辑:
     * 1.将当前AQS.state 减1
     * 2.如果state等于0,说明当前线程完全释放锁了。将exclusiveOwnerThread设为null。返回 true
     * 3.如果state不等于0,说明当前线程未完全释放锁。返回 false
     */
    protected final boolean tryRelease(int releases) {
        //减去释放的值..
        int c = getState() - releases;
        //条件成立:说明当前线程并未持锁..直接异常.,.
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        //当前线程持有锁..
        //是否已经完全释放锁..默认false
        boolean free = false;
        //条件成立:说明当前线程已经达到完全释放锁的条件。 c == 0
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        //更新AQS.state值
        setState(c);
        return free;
    }
    /**
     * AQS.unparkSuccessor 要唤醒后面的等待者线程。调用时机:释放锁的时候、取消排队的时候
     * unparkSuccessor的大体逻辑为:
     * 1.获取当前节点的waitStatus,获取当前节点.next节点,就是它的直接后继s。
     * 2.如果后继节点s 为空  || s.waitStatus 大于0(取消状态),则
     *      从后往前找,找一个位于当前节点后面的距离最近的 waitStatus小于等于0 的节点,将其封装的线程唤醒。
     *          如果找到了,就唤醒节点封装的线程 LockSupport.unpark对应的线程
     *          如果没找到,就返回了。
     * 3.如果后继节点s 不为空,则唤醒其封装的thread。LockSupport.unpark
     * 被唤醒的线程会从AQS.acquireQueued方法的parkAndCheckInterrupt处开始往下执行,自旋。
     * ......就是acquireQueued的逻辑了。 
     */
    private void unparkSuccessor(Node node) {
        //获取当前节点的状态
        int ws = node.waitStatus;
        if (ws < 0)//-1 Signal  改成零的原因:因为当前节点即将完成唤醒后继节点的任务了..
            compareAndSetWaitStatus(node, ws, 0);
        //s是当前节点 的第一个后继节点。
        Node s = node.next;
        //条件一:
        //s 什么时候等于null?
        //1.当前节点就是tail节点时  s == null。
        //2.当新节点入队未完成时(1.设置新节点的prev 指向pred  2.cas设置新节点为tail(已经算入队了)   3.(未完成)pred.next -> 新节点 )
        //需要找到可以被唤醒的节点..
        //条件二:s.waitStatus > 0    前提:s != null
        //成立:说明 当前node节点的后继节点是 取消状态... 需要找一个合适的可以被唤醒的节点..
        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;
          //上面循环,会找到一个离当前node最近的一个可以被唤醒的node。 node 可能找不到  node 有可能是null、、
        }
        //如果找到合适的可以被唤醒的node,则唤醒.. 找不到 啥也不做。
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    /**
     * 到这里为止,公平锁解锁的逻辑讲完了。下面说一下,ReentrantLock的公平锁中响应中断的lock
     * lockInterruptibly 响应中断的加锁逻辑,就是可以调用 AQS.cancelAcquire方法的。
     * 
     * 不要忘记sync的运行时状态是什么类。sync = fair ? new FairSync() : new NonfairSync();
     * 
     * 调用链是:
     * ReentrantLock.lockInterruptibly -> AQS.acquireInterruptibly -> FairSync.tryAcquire
     * -> AQS.doAcquireInterruptibly -> AQS.addWaiter 
     * -> shouldParkAfterFailedAcquire -> parkAndCheckInterrupt 
     * 前面和普通的公平锁 lock 差不多,区别在于:
     * 当前线程被中断唤醒后,会向上抛出一个异常,throw new InterruptedException();
     *     然后,走到finally块中,执行 cancelAcquire ,做取消排队的操作。
     * 响应中断的lock,才会做取消排队的操作。
     * 
     */
    /**
     * AQS.cancelAcquire方法用于
     * 取消指定node参与竞争。取消排队,出队。
     * cancelAcquire方法的大体逻辑:形参为要取消排队的node节点
     * 1.将node.thread设为null, node.waitStatus设为 1,cancelled状态
     * 2.找一个最近的 waitStatus小于等于0 的前驱节点 pred
     * 3.根据当前取消排队的node的位置不同,分情况:
     *  3.1.当前node是队尾  tail -> node
     *      通过CAS的方式将tail指向pred 节点,通过CAS的方式将pred.next改为null
     *  3.2.当前node 不是 head.next 节点,也不是 tail
     *      3.2.1.pred.waitStatus 等于-1,|| 等于0,等于0的时候,会用CAS再设为-1,保证pred.waitStatus等于-1后,
     *          就出队:pred.next -> node.next 节点后,当node.next节点 被唤醒后,
     *           调用 shouldParkAfterFailedAcquire 会让node.next 节点越过取消状态的节点
     *           完成真正出队操作。
     *      3.2.2.pred.waitStatus 等于 1,极端情况。
     *          这种情况的话,会调用unparkSuccessor方法去唤醒当前节点的后继节点,后继节点
     *          会调用shouldParkAfterFailedAcquire方法让取消状态的节点出队
     *  3.3.当前node 是 head.next节点。
     *      当前节点会调用unparkSuccessor方法唤醒后继节点。
     *      后继节点唤醒后,会调用 shouldParkAfterFailedAcquire方法,
     *      让node.next 节点越过取消状态的节点,并与 head 构建双向连接。
     *  4.最后 node.next = node; // help GC 让这个节点的next 指向自己
     */
    private void cancelAcquire(Node node) {
        //空判断..
        if (node == null)
            return;
        //因为已经取消排队了..所以node内部关联的当前线程,置为Null就好了。。
        node.thread = null;
        //获取当前取消排队node的前驱。
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        //拿到前驱的后继节点。
        //1.当前node
        //2.可能也是 ws > 0 的节点。
        Node predNext = pred.next;
        //将当前node状态设置为 取消状态  1
        node.waitStatus = Node.CANCELLED;
        /**
         * 当前取消排队的node所在 队列的位置不同,执行的出队策略是不一样的,一共分为三种情况:
         * 1.当前node是队尾  tail -> node
         * 2.当前node 不是 head.next 节点,也不是 tail
         * 3.当前node 是 head.next节点。
         */
        //条件一:node == tail  成立:当前node是队尾  tail -> node
        //条件二:compareAndSetTail(node, pred) 成功的话,说明修改tail完成。
        if (node == tail && compareAndSetTail(node, pred)) {
            //修改pred.next -> null. 完成node出队。
            compareAndSetNext(pred, predNext, null);
        } else {
            //保存节点 状态..
            int ws;
            //第二种情况:当前node 不是 head.next 节点,也不是 tail
            //条件一:pred != head 成立, 说明当前node 不是 head.next 节点,也不是 tail
            //条件二: ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
            //条件2.1:(ws = pred.waitStatus) == Node.SIGNAL   成立:说明node的前驱状态是 Signal 状态   不成立:前驱状态可能是0 ,
            // 极端情况下:前驱也取消排队了..
            //条件2.2:(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))
            // 假设前驱状态是 <= 0 则设置前驱状态为 Signal状态..表示要唤醒后继节点。
            //if里面做的事情,就是让pred.next -> node.next  ,所以需要保证pred节点状态为 Signal状态。
            if (pred != head &&
                    ((ws = pred.waitStatus) == Node.SIGNAL ||
                            (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                    pred.thread != null) {
                //情况2:当前node 不是 head.next 节点,也不是 tail
                //出队:pred.next -> node.next 节点后,当node.next节点 被唤醒后
                //调用 shouldParkAfterFailedAcquire 会让node.next 节点越过取消状态的节点
                //完成真正出队。
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                //当前node 是 head.next节点。  更迷了...
                //类似情况2,后继节点唤醒后,会调用 shouldParkAfterFailedAcquire 会让node.next 节点越过取消状态的节点
                //队列的第三个节点 会 直接 与 head 建立 双重指向的关系:
                //head.next -> 第三个node  中间就是被出队的head.next 第三个node.prev -> head
                unparkSuccessor(node);
            }
            node.next = node; // help GC 让这个节点的指针指向自己
        }
    }
    /**
     * ReentrantLock的非公平锁 NonfairSync,
     * 非公平锁也有响应中断的lock(lock)和不响应中断的lock(lockInterruptibly)
     * 公平锁和非公平锁比较类似,它们的区别在于:
     * 非公平锁lock的时候,会直接尝试用CAS 的方式 将AQS.state 从0改为1,去抢锁。没有管阻塞队列中是否有等待者线程。
     * 如果拿到锁了,就直接将exclusiveOwnerThread设为当前线程。
     * 如果拿不到,会调用tryAcquire方法。
     * tryAcquire方法在state等于0的时候,也尝试用CAS的方式去抢锁(没有管阻塞队列中是否有等待者线程),还会尝试重入锁逻辑。
     * 如果还是拿不到,就入队,挂起,等待被唤醒。
     * 非公平锁的吞吐量比公平锁要稍微好一些。
     * 公平锁在加锁的时候会判断当前队列中是否有其他线程在等待。非公平锁不会判断。
     * 
     */
    static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
    }


}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    static final class Node {       
        //枚举:独占模式
        static final Node EXCLUSIVE = null;
        //表示当前节点处于 取消 状态
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        //注释:表示当前节点需要唤醒他的后继节点。(SIGNAL 表示其实是 后继节点的状态,需要当前节点去喊它...)
        static final int SIGNAL    = -1;
        //node状态,可选值(0 , SIGNAl(-1), CANCELLED(1), CONDITION, PROPAGATE)
        // waitStatus == 0  默认状态
        // waitStatus > 0 取消状态(在ReentrantLock模式下)
        // waitStatus == -1 表示当前node如果是head节点时,释放锁之后,需要唤醒它的后继节点。
        volatile int waitStatus;

        //因为node需要构建成  fifo 队列, 所以 prev 指向 前继节点
        volatile Node prev;

        //因为node需要构建成  fifo 队列, 所以 next 指向 后继节点
        volatile Node next;

        //当前node封装的 线程本尊..
        volatile Thread thread;

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        
        //......
    }    
    
    //头结点 任何时刻 头结点对应的线程都是当前持锁线程。这个线程是活跃状态的
    private transient volatile Node head;
    //阻塞队列的尾节点   (阻塞队列不包含 头结点  head.next --->  tail 认为是阻塞队列)
    private transient volatile Node tail;
    //表示资源
    //独占模式:0 表示未加锁状态   >0 表示已经加锁状态
    private volatile int state;

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
    /**
     * 就是给当前节点找一个好爸爸,给爸爸设为 -1
     * 总结:
     * 1.当前节点的前置节点是 取消状态 ,第一次来到这个方法时 会越过 取消状态的节点, 第二次 会返回true 然后park当前线程
     * 2.当前节点的前置节点状态是0,当前线程会设置前置节点的状态为 -1 ,第二次自旋来到这个方法时  会返回true 然后park当前线程.
     *
     * 参数一:pred 当前线程node的前置节点
     * 参数二:node 当前线程对应node
     * 返回值:boolean  true 表示当前线程需要挂起..
     */
    //AQS#
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取前置节点的状态
        //waitStatus:0 默认状态 new Node() ; -1 Signal状态,表示当前节点释放锁之后会唤醒它的第一个后继节点; >0 表示当前节点是CANCELED状态
        int ws = pred.waitStatus;
        //条件成立:表示前置节点是个可以唤醒当前节点的节点,所以返回true ==> parkAndCheckInterrupt() park当前线程了..
        //普通情况下,第一次来到shouldPark。。。 ws 不会是 -1
        if (ws == Node.SIGNAL)
            return true;

        //条件成立: >0 表示前置节点是CANCELED状态
        if (ws > 0) {
            //找爸爸的过程,条件是什么呢? 前置节点的 waitStatus <= 0 的情况。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //找到好爸爸后,退出循环
            //隐含着一种操作,CANCELED状态的节点会被出队。
            pred.next = node;

        } else {
            //当前node前置节点的状态就是 0 的这一种情况。
            //将当前线程node的前置node,状态强制设置为 SIGNAl,表示前置节点释放锁之后需要 喊醒我..
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    //AQS#parkAndCheckInterrupt
    //park当前线程 将当前线程 挂起,唤醒后返回当前线程 是否为 中断信号 唤醒。
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }


    //......
}

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
    //独占模式下表示当前持有锁的线程
    private transient Thread exclusiveOwnerThread;    

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    //...... 
}

图片

  开发工具 最新文章
Postman接口测试之Mock快速入门
ASCII码空格替换查表_最全ASCII码对照表0-2
如何使用 ssh 建立 socks 代理
Typora配合PicGo阿里云图床配置
SoapUI、Jmeter、Postman三种接口测试工具的
github用相对路径显示图片_GitHub 中 readm
Windows编译g2o及其g2o viewer
解决jupyter notebook无法连接/ jupyter连接
Git恢复到之前版本
VScode常用快捷键
上一篇文章      下一篇文章      查看所有文章
加:2021-08-28 09:33:21  更:2021-08-28 09:33:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/22 23:13:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码