zoukankan      html  css  js  c++  java
  • [源码分析]ReentrantLock & AbstractQueuedSynchronizer & Condition

    首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前面加了"My". 比如把ReentrantLock改名为了MyReentrantLock, 在源码分析的章节里, 我基本不会对源码进行修改, 所以请忽视这个"My"即可.


    一. 简介

    锁是什么? 锁是一种标志, 或者是一种资源, 持有锁的线程才可以继续往下执行相应的内容. 未持有锁的线程需要等待这个锁资源. 直到获取到了这个锁, 才可以继续向下执行.

    0. ReentrantLock的一个小demo

    想自己运行这段代码的话, 把代码中的"MyReentrantLock" 改为 "ReentrantLock" 即可. (后续的代码也一样, 如果想自己运行, 还编译报错, 请把我修改的代码改回来. 也就是把"My"都去掉就好了)

    public class Main {
        private static MyReentrantLock lock = new MyReentrantLock();
    
        public static void main(String[] args) throws Exception {
            // 场景如下: 线程1先获得锁, 释放后, 线程2 再获得锁.
    
            new Thread(() -> {
                System.out.println("线程1启动");
                lock.lock();
                System.out.println("线程1抢到锁");
                try {
                    System.out.println("这里是业务逻辑1");
                    quietSleep(2);// 两秒后释放锁
                    System.out.println("两秒后");
                } finally {
                    lock.unlock();
                    System.out.println("线程1释放锁");
                }
            }).start();
    
            new Thread(() -> {
                System.out.println("线程2启动");
                quietSleep(1); // 在这里进行谦让. 确保上面的线程能先运行. 也就是让上面的线程先获得锁
                lock.lock();
                System.out.println("线程2抢到锁");
                try {
                    System.out.println("这里是业务逻辑2");
                } finally {
                    lock.unlock();
                    System.out.println("线程2释放锁");
                }
            }).start();
    
        }
    
        public static void quietSleep(long sec) {
            try {
                Thread.sleep(sec * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    }
    

     输出的结果如下:

    1. sync字段

    首先来看一下ReentrantLock里唯一的一个字段

    Sync继承自AQS(AbstractQueuedSynchronizer, 以下简称AQS) . 公平锁和非公平锁都继承了Sync. Sync是ReentrantLock类里锁的统一声明. 

    2. lock/unlock依赖Sync

    ReentraintLock的 lock()和unlock()方法实际上都是靠Sync来实现的:

    3. 锁内部类定义

    Sync 和 公平锁 和 非公平锁 都是ReentrantLock的内部类, 类的定义部分如下(细节先隐藏起来了, 后面会讲):

    4. ReentrantLock构造器

    ReentrantLock有两个构造器.

    1. 默认构造器是直接使用了非公平锁. 非公平锁就是不一定按照"先来后到"的顺序来进行争抢.

    2. 带参构造器可以传递一个bool类型. true的时候为公平锁. 公平锁就是按照"先来后到"的顺序来进行争抢.

    二. 公平锁申请锁

    使用锁的第一个步骤, 当然就是先申请锁了, 咱么来分析一下源码, 看看申请锁的流程吧. 

    1. 公平锁获取锁的流程(单线程, 没有争抢) 

    首先从最外层的调用lock()方法开始. 咱们在Main方法里写下这两行代码:

    MyReentrantLock就是ReentrantLock, 我复制了源代码, 然后改了个名字而已.

    Reentraint类的lock()方法最终还是调用的sync.lock()

    由于我们现在使用的是公平锁. 所以sync现在是FairSync. 所以sync.lockI()实际上就是FairSync类里的lock()方法

    发现lock()调用的是acquire(1)这个方法, 这个方法是在AQS类里实现的.代码如下:

    arg当时传进来的是1, 所以首先进行的是tryAcquire(1)来进行"尝试获取锁"的操作. 这时一种乐观的想法.

    tryAcquire方法的具体实现在FairSync类里, 具体代码如下:

    /**
         * @return 返回true: 获取到锁; 返回false: 未获取到锁
         * 什么时候返回true呢?  1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
         * @implNote 尝试直接获取锁.
         */
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程的引用
            final Thread current = Thread.currentThread();
    
            // 当前锁的计数器. 用于计算锁被获取的次数.在重入锁中表示锁重入的次数.由于这个锁是第一次被获取, 所以c==0
            int c = getState();
    
            // c==0, 也就是 state == 0 ,重入次数是0, 表示此时没有线程持有锁.
            if (c == 0) {
                // 公平锁, 所以要讲究先来后到
                // 因为有可能是上一个持有锁的线程刚刚释放锁, 队列里的线程还没来得及争抢, 本线程就乱入了
                // 所以每次公平锁抢锁之前, 都要判断一下等待队列里是否有其他线程
                if (!hasQueuedPredecessors() &&
                        // 执行到这里说明等待队列里没有其他线程在等待.
                        // 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
                        // 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=
                        compareAndSetState(0, acquires)) {
                    
                    // 到这里就获取到锁了,标记一下,告诉大家,现在是我(当前线程)占用了锁
                    setExclusiveOwnerThread(current);
                    // 成功获取锁了, 所以返回true
                    return true;
                }
    
    
                //-- 由于现在模拟的是单纯地获取一次锁, 没有重入和争抢的情况, 所以执行不到这里, 上面的cas肯定会成功, 然后返回true
    
    
            } else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

    争抢完锁之后会返回true, 然后回到上层方法acquire : 

    if语句里 && 前面是false, 不会继续往下执行了. 当前线程获取到了锁, 而且执行了所有该执行的内容, 就完事儿了.

    2. 公平锁进行重入的流程

    重入就是一个线程获取到了锁, 然后这个线程又一次申请(进入)了这个锁.

    重入用synchronized来举例就是这样:

    用ReentrantLock来举例子就是这样:

    同一个线程(main线程) 首先进行了lock.lock()申请并占有了锁, 随后又执行了一次lock.lock(). 还没释放锁的情况下, 又一次申请锁. 这样就是重入了.  

    上面一小节已经分析了第一行的lock.lock()是如何获取到锁的, 所以我们只分析 重入的部分, 也就是后面那句lock.lock()的执行流程.

    前面的执行过程一直是一模一样的, 直到这里:

    /**
         * @return 返回true: 获取到锁; 返回false: 未获取到锁
         * 什么时候返回true呢?  1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
         * @implNote 尝试直接获取锁.
         */
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程的引用
            final Thread current = Thread.currentThread();
    
            // 当前锁的计数器. 由于前面的那句lock已经获取到锁了, 所以这里是status==1, 也就是 c==1
            int c = getState();
    
            // c==1, 表示当前有线程持有锁, 所以这段if是进不去了
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
    
            // 由于 c==1 , 无法进入if语句, 所以来看看满不满足这里的 else if
            // 这个锁被人占了, 但还是不死心, 于是看一下是不是当前线程自己占的这个锁.
            // (人家女生说有喜欢的人, 为什么不问问是不是自己呢 = =.)
            // 由于是同一个线程, 所以就是自己啦! 所以会进入这个else if分支,
            } else if (current == getExclusiveOwnerThread()) {
                // 代码执行到这里了, 就是所谓的 重入 了
    
                // 这里的acquires的值是1, 所以nextc =  1 + 1 , 也就是2了
                int nextc = c + acquires;
                // 小于0, 说明int溢出了
                if (nextc < 0) throw new Error("Maximum lock count exceeded");
                // 在这里把状态更新一下, 把state更新为2, 意思就是这个锁被同一个线程获得2次了. 
                // (大家就可以以此类推, 下次再重入的话, 那么就会再+1, 就会变为3....)
                setState(nextc);
                // 重入完成, 返回true
                return true;
            }
            
            return false;
        }
    

     还记得上小节讲的, 获取锁的时候进入的是这段代码的if语句, 而重入就不一样了, 进入的是 else if语句. 但最终返回的还是true, 表示成功. 

    上面讲的是无争强的情况, 接下来讲讲有争抢的情况.

    3. 公平锁cas争抢失败

    场景如下:

    一开始锁是空闲状态, 然后两个线程同时争抢这把锁(在cas操作处发生了争抢).

    一个线程cas操作成功, 抢到了锁; 另一个线程cas失败. 

    代码例子如下(代码的意思到位了, 但是这段代码最后不一定会在cas处进行争抢, 大家意会就好了):

    cas操作成功的线程就和上面第1小节的一样, 就不用再重复描述了.

    而cas争抢失败的线程会何去何从呢? 看我给大家分析: 

     /**
         * @return 返回true: 获取到锁; 返回false: 未获取到锁
         * 什么时候返回true呢?  1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
         * @implNote 尝试直接获取锁.
         */
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程的引用
            final Thread current = Thread.currentThread();
    
            // 当前锁的计数器.
            int c = getState();
    
            // state == 0 表示此时没有线程持有锁
            if (c == 0) {
                // 本场景中, 一开始锁是空闲的, 所以队列里没有等待的线程
                if (!hasQueuedPredecessors() &&
                        // 两个线程在这里进行争抢
                        // cas抢成功的会进入到if代码块
                        // cas抢失败的, 就跳出整个if-else, 也就是直接到最后一行代码
                        compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            } else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
    
            // cas 操作失败后, 会这直接执行到这里. 返回false.
            return false;
        }
    

     在这里返回了false, 回到上一层函数.

    第一个条件是true, 所以会继续往下执行acquireQueued方法. 来准备让这个失败的线程进入队列等待.

    下面继续来给大家讲解 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) .

    先讲讲这个addWaiter(Node.EXCLUSIVE):

    /**
         * 将当前线程封装为Node, 然后根据所给的模式, 进行入队操作
         *
         * @param mode 有两种模式 Node.EXCLUSIVE 独占模式, Node.SHARED 共享模式
         * @return 返回新节点, 这个新节点封装了当前线程.
         */
        private Node addWaiter(Node mode) { // 这个mode没用上.
            Node node = new Node(Thread.currentThread(), mode);
            // 咱们刚才都没见到过tail被赋予了其他的值, 当然就是null了.
            Node pred = tail;
            // tail是null的话, pred就是null, 所以不会进入到这个if语句中.所以跳过这个if语句.
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
    
            // 因为锁的等待队列是懒初始化, 直到有节点插入进来, 它才初始化.
            // 而现在这个挣钱失败的线程, 正好是锁建立以来, 第一个进入等待队列的线程. 所以现在才准备进行初始化.
            // 初始化完了后会把当前线程的相关信息和引用封装成Node节点, 然后插入到队列当中.并且制定head 和 tail.
            // tail就不等于null了, 所以下一次addWaiter方法被调用的时候, 就会执行上面的if语句了. 而不会跳过if语句, 来到这里进行初始化了.
            enq(node);
            // 返回这个Node节点.
            return node;
        }
    

     目的就是要将这个cas失败的线程封装成节点, 然后插入到队尾中. (等待队列是懒初始化,) 

    如果队列已经初始化了, 那么tail就不会是null, 就会执行上面代码中的if语句, 调整一下指针的引用就好了.

    但是如果队列还未初始化, 那么就应该先初始化, 再插入. 先初始化,再插入, 对应的代码是enq(node). 

    接下来讲解一下enq方法: 

       /**
         * 采用自旋的方式入队
         * CAS设置tail,直到争抢成功.
         */
        private Node enq(final Node node) {
            for (; ; ) {
                Node t = tail;
                //  最开始tail肯定是null, 进入if进行初始化head和tail.
                if (t == null) { // Must initialize
                    // 设置head 和tail. cas来防止并发.
                    if (compareAndSetHead(new Node())) tail = head;
                    
                // if 语句执行完了后, 之后的for循环就会走else了.
                } else {
                    // 争抢入队, 没抢到就继续for循环迭代.抢成功了就可以return了,不然一直循环.
                    // 为什么是用cas来争抢呢? 因为怕是多个线程一起执行到这里啊 
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
    

     刚才的addWaiter(Node.EXCLUSIVE) 分析完了, 总之就是addWaiter之后, 队列肯定是被创建完了, 而且还把node(当前线程的封装)插入到了队列的队尾. 并且返回了这个node.  acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 可以简化为 acquireQueued(node)

    所以继续分析acquireQueued方法.

    final boolean acquireQueued(final Node node, int arg) {
            // node是刚才addWaiter方法插入到队尾的节点
            // arg 是 1
    
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (; ; ) {
                    // 获取node节点的前驱.
                    final Node p = node.predecessor();
    
                    // 如果node节点的前驱是head
                    if (p == head
                            // 那么可以再尝试着抢一下锁. 
                            // 等待队列里的第一个节点很乐观, 因为确实很有可能会马上轮到他
                            && tryAcquire(arg)) {
                        // 如果这个node就是那么巧合, 刚刚锁被释放了, 这回重新抢就真的抢到了
                        // 那么就把当前节点设为头结点.(头结点的含义就是当前持有锁的线程)
                        setHead(node);
                        // 上一个节点既然已经释放了锁, 也就该GC了. 置为null, 方便GC收集
                        p.next = null; // help GC
                        // 很明显是获取锁成功了啊, 所以failed = false
                        failed = false;
    
                        // 这么大一段代码, 只有这一处return
                        return interrupted;
                    }
    
                    //---- 如果不是队头,  那么就会执行到这里.
                    //---- 或者虽然作为等待队列里的第一名, 单由于持有锁的线程还是没有释放, 所以还是没抢到锁. 那么也会执行到这里
    
                    // 获取锁失败的时候是否该阻塞
                    if (shouldParkAfterFailedAcquire(p, node)
                            // 在这里阻塞, 等待唤醒
                            && parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                // 上面那段, 如果中途异常了的话, 就会执行到这里. (一般不会到这里的)
                if (failed) cancelAcquire(node);
            }
        }
    

     上面这段代码中shouldParkAfterFailedAcquire方法 和 parkAndCheckInterrupt() 方法 还未解释. 一个一个来.

    /**
         * 当前线程没有抢到锁,是否需要挂起当前线程
         *
         * @param pred 前驱结点
         * @param node 当前结点
         * @return 如果线程需要被阻塞, 那么就返回true
         */
        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            int ws = pred.waitStatus;
            // 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
            if (ws == Node.SIGNAL)
                return true;
            // 大于0, 其实就是等于1, Node.CANCELLED 是 1, 因为状态中只有这个状态是大于0的...说明前驱节点取消了排队
            // 所以下面这块代码说的是, 在链表中从prev结点开始, 往前删掉CANCELLED状态的结点.
            // 只有CANCELLED状态值大于0
            if (ws > 0) {
                do {
                    node.prev = pred = pred.prev;
                    
                // 删掉之后再往前看看, 看看前面是不是CANCELLED, 如果是, 那还得继续往前删
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
                // 在前面的两个if语句中排除掉了waitStatus值为-1和1的情况,
                // 只剩下0,-2,-3这三个状态了
                // 然而在我们前面的源码中,都没有看到有设置waitStatus的,
                // 所以只剩下等于0的情况了
                // 下面的操作就是, 如果waitStatus等于0, 那么就用cas将前驱结点的waitStatus设置为-1
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }
    

    shouldParkAfterFailedAcquire里的前两行是在判断前驱节点prev的状态. 但是之前咱们分析代码, 并没有发现哪里设置了waitState.

    所以waitState是默认值0.

    所以shouldParkAfterFailedAcquire会直接执行下面的else, 在这里吧pred的waitState设置为-1, 然后返回false.

    回到刚才的acquireQueued方法. 由于外层是for循环, 会在下一次for循环在此执行到shouldParkAfterFailedAcquire方法. 

    由于刚才已经把前驱节点prev的waitState改为1了, 所以这次在前两行判断prev的waitState时, 直接就满足条件, 然后return true了.

    shouldParkAfterFailedAcquire方法return true了, 才会往下执行parkAndCheckInterrupt方法.

    下面是parkAndCheckInterrupt()方法. 最终返回Thread.interrupted(). 返回线程是否被中断. (中断和挂起不是一回事 )

        /**
         * 在这里线程阻塞.
         * 被唤醒的时候会返回, 如果被中断过, 那么就返回true
         *
         * @return {@code true} if interrupted
         */
        private final boolean parkAndCheckInterrupt() {
            // 挂起.
            MyLockSupport.park(this);
            return Thread.interrupted();
        }
    

     LockSupport.park(this)会挂起当前线程. 但是LockSupport.park还有一个隐藏功能. 就是, 如果先对一个线程unpark, 再对这个线程park, 那么这次的park是失效的. 下一次park才会挂起.

    原因就是, 对一个没有被park的线程进行unpark的时候, 会把标志位perm置为1. 而每次park的操作, 都是先去检查perm是否为1.

    如果是1, 那么置为0, 并且这次不挂起.

    如果perm为0, 那么就直接挂起这个线程.

    4. 公平锁由于队列内有元素而失败

    demo如下. 前两个线程, 其中一个获取锁成功, 另一个失败, 然后进入等待队列.

    稍后, 第三个线程来获取锁, 但是这时由于等待队列中已经有元素在等待了. 所以会直接失败, 然后会被插入到等待队列的尾部.

    上面的main方法中总共有三个线程想要占有锁. 前两个锁的争抢在上小节就已经模拟过了.

    咱么现在只分析第三个线程申请锁的流程. 这个场景下的tryAcquire方法如下(会直接返回false):

    /**
             * @return 返回true: 获取到锁; 返回false: 未获取到锁
             * 什么时候返回true呢?  1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
             * @implNote 尝试直接获取锁.
             */
            protected final boolean tryAcquire ( int acquires){
                // 获取当前线程的引用
                final Thread current = Thread.currentThread();
    
                // 当前锁的计数器.
                int c = getState();
    
                // 不会走这的if语句, 因为锁被其他线程占有, 肯定不是0
                if (c == 0) {
                    if (!hasQueuedPredecessors() &&
                            compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
    
                } else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
    
                // 由于队列内有元素, 所以if语句不执行, 
                // 由于不是重入, else if 也不执行.
                // 直接返回false
                return false;
            }
    

    这段方法返回false, 说明需要执行这个. acquireQueued(addWaiter(Node.EXCLUSIVE), arg). 先看看addWaiter方法有什么区别.

    /**
         * 将当前线程封装为Node, 然后根据所给的模式, 进行入队操作
         *
         * @param mode 有两种模式 Node.EXCLUSIVE 独占模式, Node.SHARED 共享模式
         * @return 返回新节点, 这个新节点封装了当前线程.
         */
        private Node addWaiter(Node mode) {
            Node node = new Node(Thread.currentThread(), mode);
            // 以下几行代码想把当前node加到链表的最后面去,也就是进到阻塞队列的最后
            Node pred = tail;
            // 如果tail不是空, 说明有头结点.说明这个队列已经被初始化了.
            // 因为本小节讲的就是: 因为公平锁的等待队列中有其他线程才导致当前线程争锁失败, 所以说明等待队列不仅被初始化了, 而且里面还有元素.
            if (pred != null) {
                // node设置自己的前驱为pred
                node.prev = pred;
                // 用CAS把当前节点node设置为队尾, 如果成功后,tail指针就指向了node
                if (compareAndSetTail(pred, node)) {
                    // 如果cas争抢成功, 那么就会在这里返回.(而cas失败的, 会跳过这个if代码块, 会执行到下面的enq方法)
                    // 剩下的就是整理一下链表数据结构的连接问题了
                    // pred调整自己的后继为node
                    pred.next = node;
                    return node;
                }
            }
    
            // 如果在上面的cas中设置失败, 那么还是会执行到这里.
            // 然后在enq方法里靠for循环+cas的形式, 不断尝试着插入到队尾.
    
            enq(node);
            return node;
        }
    

     后续执行的就和上小节的一样了.就不重复了...

    当然, 场景是举不完的, 举完的话就跟笛卡尔积那样了. 我这里只是靠这四个例子来尽量完整地分析了获取锁的流程.

    三. 公平锁释放锁

    刚才申请锁的流程. 但是争抢失败的那些线程, 最后都进入到了等待队列里, 然后就杳无音讯了.

    那当前持有锁的线程释放锁后, 是如何唤醒等待队列里的线程, 让下一个线程获取锁的呢?

    咱么接下来分析一下释放锁的过程吧.

    1. 申请1次锁, 执行一些业务, 然后释放

    咱们只关注unlock, lock就跳过了, 前面讲过了.

    ReentrantLock类的lock()方法 代码如下: 

      

    而这个release是AQS里的方法. 源码如下:

    其中arg变量值是1.  首先会执行tryRelease(1) 来尝试释放锁.

    如果尝试成功了, 那么tryRelease(1)就会返回true, 就会继续执行if代码块里的内容. 

    如果尝试失败了, 那么tryRelease(1)就会返回false. 然后就会跳过if语句, 最终本段方法(release方法)也会返回false.

    咱们先分析一下tryRelease方法吧(tryRelease方法的源码在Sync抽象类里):

    protected final boolean tryRelease(int releases) {
            // releases == 1
    
            // c 就是重入次数 -1 , 由于本场景下模拟的是简单的获取一次锁, 然后释放, 不涉及到重入. 所以getState() == 1
            // 所以c = 1 - 1 , c现在等于0
            int c = getState() - releases;
    
            // 判断当前的线程是不是持有锁的线程, 不然抛异常.
            // 这是为了其他的线程捣乱.
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
    
            // 用于标记是否可以完全释放锁
            boolean free = false;
    
            // c等于0, 说明没有重入了, 可以完全释放了.
            if (c == 0) {
                // 标记一下, 准备完全释放锁
                free = true;
                // 把锁的持有者设置为空, 表示锁被释放.
                setExclusiveOwnerThread(null);
            }
            // 把刚才c==0 设置为state
            setState(c);
    
            // 表示是否完全释放. 本场景下返回true
            return free;
        }
    

     由于返回的是true, 所以返回后还有if语句块要执行:

    接下来分析一下其中的unparkSuccessor方法, 看看他是如何唤醒下一个节点的.(这个方法在AQS里)

    unpark之后, 就会把之前park(挂起)的线程激活, 然后继续执行:

    如果线程被中断了, 那么parkAndCheckInterrupt()方法会返回true, 然后就会执行interrupted = true 这句话. 

    挂起和中断不是一回事, 一般不会被中断的. 所以一般不会执行interrupted=true这句话. 

    外层是个for循环, 当前线程被激活后, 作为等待队列中的第一个线程, 来进行获取锁. 由于是公平锁, 所以可以放心拿到, 没有人会抢, 所以会正常获取到锁.

    2. 重入锁的释放

     

    释放重入的锁(同一个线程多次获取的锁), 执行流程唯一不同的就是tryRelease方法了, 其他的都一样, 可以直接参考上面一小节的.

    咱么看看重入的时候, tryRelease是如何执行的吧.

     protected final boolean tryRelease(int releases) {
            // 其实就是重入计数器 -1
            // 而由于本线程获取了2次这个锁, 所以state字段的值为2
            // 所以c = 2 - 1
            // 所以现在c == 1
            int c = getState() - releases;
    
            // 判断当前的线程是不是持有锁的线程, 不然抛异常.
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
    
            // 用来标记是否完全释放锁
            boolean free = false;
    
            // c现在等于1, 不会进入这个if代码块
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
    
    
            // 设置重入计数器, 也就是让 state =1 
            setState(c);
    
            // 返回false.
            return free;
        }
    

     本小节和上小节的区别就是这段代码了.既然这段方法返回false, 那么返回后, release方法的if代码块自然也就不执行了. 

    四. 非公平锁的获取

    刚才讲解了公平锁, 那么接下来讲讲非公平锁, 到底是怎么个不公平呢?

    1. 非公平锁与公平锁获取的区别

    由于非公平锁的获取与公平锁的获取, 只有一点点区别. 所以咱么只分析出区别就好了, 其他的部分都一样的.

    然后会调用到NonfairSync类里的lock()方法.

    这里就体现出了区别.

    公平锁里的lock()方法里面, 只有acquire(1). 

    而非公平锁在acquire(1)之前多了一次cas操作. 一上来就尝试着抢占锁, 看看有没有机会(万一真的这个时候持有锁的线程正好把锁释放了呢). 非公平锁根本不管是否有其他人在排队.上来就是一抢.

    当这次cas失败了, 才会像公平锁一样进入acquire(1)方法:

     

    这里和公平锁一样. 只是, 非公平锁的tryAcquire方法和公平锁的tryAcquire方法内部实现不一样.

    看看非公平锁的tryAcquire方法吧:

    咱们继续往下看看nonfairTryAcquire方法吧:

    /**
             * 不公平地尝试获取锁.
             * 不公平的语义就是: 不用判断队列里是否有其他线程在等待, 直接抢.
             */
            final boolean nonfairTryAcquire(int acquires) {
                // 获取当前线程的引用
                final Thread current = Thread.currentThread();
                // 当前线程的重入次数
                int c = getState();
                // 如果是0, 表示此时此刻锁还被被任何一个线程所占用
                if (c == 0) {
                    // 当c==0的时候, 公平锁锁是先判断队列里是否有其他线程在等待, 如果没有, 再去cas争抢.
                    // 而非公平锁这里, 就是根本就不去理会等待队列, 自己抓到机会就赶紧抢
                    // cas来争抢, 让重入次数变1.
                    // 用cas是因为这个地方会发生并发.
                    // 多个抢占当然只有一个成功了
                    if (compareAndSetState(0, acquires)) {
                        // 设置锁的拥有者为当前线程.
                        setExclusiveOwnerThread(current);
                        return true;
                    }
    
                    // 如果不是0, 说明锁被某一个线程占用了
                    // 既然被占用了, 那就有两种情况: 1. 被自己占用; 2. 被别的线程占用
                    // 所以先看看是不是自己占用的, 如果是自己占用的, 那就重入.
                } else if (current == getExclusiveOwnerThread()) {
                    // 其实就是+1
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    // 这里不会产生争抢, 不必用cas
                    // 因为只有占用锁的这一个线程才能进入到这个else if 里
                    // 一个线程不可能发生争抢
                    setState(nextc);
                    return true;
                }
                // 1. 如果在if里的cas争抢失败
                // 2. 或者是不满足else if的条件
                // 那就会直接返回false
                // 不管是成功还是失败, 都不会有线程的等待阻塞之类的. 都是立即返回.
                return false;
            }
    

     这里的非公平锁的nonfairTryAcquire方法 和 公平锁的tryAcquire方法很像. 区别就是:

    非公平锁是, 当c==0. 也就是此时此刻, 锁是空闲状态的时候. 直接就尝试着用cas来争抢锁, 看看是否能成功, 而不管等待队列是否还有其他线程再等待.

    而公平锁在c==0的时候, 也就是state==0 的时候, 先去看看队列里是否有其他线程再等待, 如果队列里没有其他线程在等待, 才会去cas争抢. 不然就会把机会让给队列里的第一个线程, 而自己会进入到等待队列的尾部.

    为什么c==0了, 队列里还有可能会有其他的元素在等待呢?

    因为c==0只是说明当前锁的状态是空闲状态. 只是上一个线程刚刚把锁释放, 当前线程就来争抢锁了, 还没来得及唤醒等待队列里的第一个线程呢.  

    其他地方就跟公平锁都一样的, 就是多了本小节讲的两处cas. 

    五. 非公平锁的释放

    1. 非公平锁会导致饥饿

    也就是说, 上一个线程释放锁后, `等待队列` 里的第一个线程就会被激活, 然后会执行tryAcquire方法. 如果这个时候有新的线程来争抢, 

    由于是非公平模式, 有可能新的线程会抢到这个锁. 如果新的线程抢到了锁, 那么刚刚被激活的线程(等待队列里的第一个线程)就是执行tryAcquire失败, 这个方法执行失败就意味着会被再次被挂起. 如果并发量严重, 很可能`等待队列`里的所有线程在一定时间内都无法被正常调度.也就是产生了线程饥饿的现象.

    六. Condition简介

    1. condition简介和demo

    public class Main {
        private static MyReentrantLock lock = new MyReentrantLock();
        private static Condition condition = lock.newCondition();
    
        public static void main(String[] args) {
            new Thread(Main::funcA).start();
            new Thread(Main::funcB).start();
        }
    
        public static void funcA() {
            lock.lock();
    
            System.out.println("await之前");
            try {
                condition.await(); // 在这里等待被其他线程通知(signal)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("await之后");
    
            lock.unlock();
        }
    
        public static void funcB() {
            lock.lock();
    
            System.out.println("signal之前");
            condition.signal(); // 这里会通知funcA的await().让funcA()继续执行下去
            System.out.println("signal之后");
    
            lock.unlock();
        }
    }
    

     运行这段代码, 输出如下: 

    如果还是没有体会到区别, 那么把main方法里的第二行注释掉, 然后再执行一下:

    输出结果如下: 

    也就是说, await()会使当前线程挂起, 需要其他线程通知他, 他才能被激活(唤醒).

    七. Condition实例化

    1. 获取condition的例子

    2. condition实例化的源码

    newCondition方法在ReentrantLock类里的实现如下:

    Sync类里的newCondition()方法如下:

    ConditionObject是AQS里的一个内部类,实现自Condition,  类的声明如下(具体源码后面再解释):

    八. condition的等待(await) 和 通知(signal)

    1. 只执行一句await()后的流程

     

    await()方法的具体实现在AQS里的内部类ConditionObject类里:

    public final void await() throws InterruptedException {
            if (Thread.interrupted()) throw new InterruptedException();
            // 添加到 condition 的`条件队列`中
            Node node = addConditionWaiter();
            // 完全释放锁,返回值是释放锁之前的 state 值
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 这里的isOnSyncQueue就是在判断node节点是否在锁的`等待队列`里
            while (!isOnSyncQueue(node)) {
                // 在这里线程挂起
                MyLockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //---- 程序不会执行到下面, 因为在前面就已经挂起了.
    
    
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
    

     这里面有几个方法之前没提到过, 在这里一一攻破.

    先解决addConditionWaiter()方法:

       /**
         * 将当前线程对应的节点入队,插入队尾, 并且作为本方法的返回值.
         */
        private Node addConditionWaiter() {
            // 本例子中的场景下, 只执行过一次await()方法, 所以是第一个进入`条件队列`的元素.
            // 所以lastWaiter和firstWaiter肯定都是null.
            Node t = lastWaiter;
            // 本例子中t==null, 所以这段if暂时不考虑吧
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            
            // 新建节点
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            
            // 因为t==null, 意思是队列目前还是空的, 所以这个节点是第一个节点, 所以是firstWaiter.
            if (t == null) firstWaiter = node;
            else t.nextWaiter = node;
            
            // 但node同时也是最后一个节点, 也就是lastWaiter
            lastWaiter = node;
            
            // 最后会返回本方法
            return node;
        }
    

     接下来是fullRelease(node)方法, 来完全释放锁:

    final int fullyRelease(Node node) {
            boolean failed = true;
            try {
                int savedState = getState();
                // 这里使用了当前的 state 作为 release 的参数,也就是完全释放掉锁,将 state 置为 0
                if (release(savedState)) {
                    failed = false;
                    // 并且把释放锁之前的state值返回出去. (本例子中是1)
                    return savedState;
                } else {
                    throw new IllegalMonitorStateException();
                }
            } finally {
                if (failed)
                    node.waitStatus = Node.CANCELLED;
            }
        }
    

     最后就是isOnSyncQueue(node)方法, 来判断锁的`等待队列`中有没有当前这个node:

       /**
         * 这个方法就是判断 node 是否已经移动到sync queue了
         * (signal 的时候会将节点从条件队列移到sync queue)
         */
        final boolean isOnSyncQueue(Node node) {
            // 当进入Condition队列时,waitStatus肯定为CONDITION,
            // 如果同时别的线程调用signal,Node会从Condition队列中移除,
            // 并且移除时会清除CONDITION状态。
            // 从移除到进入sync queue队列,中间这段时间prev必然为null,所以还是返回false,即被park
            if (node.waitStatus == Node.CONDITION || node.prev == null)
                // 本例子中, 会在这里返回
                return false;
    
            
            //--- 本例子中, 程序不会往下执行了. 但是下面的代码还是分析一下吧. 这样待会儿就不用再重新讲个方法了.
            
            
            // 当别的线程进入sync queue队列时,会和前一个Node建立前后关系,所以如果next存在,说明一定在release队列中
            if (node.next != null) // If has successor, it must be on queue
                return true;
    
            // 到这里还没找到, 那只能去锁的`等待队列`里一个一个找了
    
            // 可能该Node刚刚最后一个进入release队列,所以是tail,其next必然是null,所以需要从队尾向前查找
            // 这个方法的源码就不讲了, 太简单了, 就是链表从后往前找node.找到了就true.没找到就false.
            return findNodeFromTail(node);
        }

     最终会执行到await()方法里的park()方法, 线程挂起. 等待被别的线程唤醒.

    2. 只执行一句signal()后的流程

    然后咱们看看signal()的源码.

    由于firstWaiter==null, 所以first==null, signal方法直接就退出了.

    3.一个线程await等待, 另一个线程用signal来唤醒

    本场景的程序demo如下: 

    public class Main {
        private static Scanner scanner = new Scanner(System.in);
    
        private static MyReentrantLock lock = new MyReentrantLock();
        private static Condition condition = lock.newCondition();
    
        public static void main(String[] args) throws Exception {
            new Thread(Main::funcA).start();
            new Thread(Main::funcB).start();
        }
    
        public static void funcA() {
            lock.lock();
    
            System.out.println("await之前");
            try {
                condition.await(); // 在这里等待被其他线程通知(signal)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("await之后");
    
            lock.unlock();
        }
    
        public static void funcB() {
            lock.lock();
    
            System.out.println("signal之前");
    
            System.out.print("请输入任意内容并回车, 以执行signal方法: ");
            scanner.next(); // 在这里进行阻塞, 在控制台输入任意内容后回车, 就会接触阻塞, 就会执行signal方法, 也就是通知funcA()方法.
            condition.signal(); // 这里会通知funcA的await().让funcA()继续执行下去
            System.out.println("signal之后");
    
            lock.unlock();
        }
    
    }
    

     首先, await()仍然执行到park这句, 然后挂起, 这点与本章第1小节的流程是一样的(看下图, 我选中的park那行代码, await就在这里挂起): 

    而此时控制台如下:

    此时还没有执行signal, 因为我用输入流给signal方法进行阻塞了, 需要输入内容后回车, 就可以调用到signal方法.signal通知后,await就会被唤醒.

    如下:

    咱们分析一下signal是如何通知await, 然后让await线程被唤醒的:

    因为刚才执行过await(), 所以firstWaiter不会是null. 所以会调用到doSignal方法:

     

    上面这段代码也比较简单, 就是将firstWaiter为头的这个链表, 把第一个元素出队, 然后让第二个元素当新的头部. 然后让刚才出队的那个元素执行tansferForSignal方法.

        /**
         * 将节点从条件队列转移到锁的`等待队列`
         *
         * true 代表成功转移
         * false 代表在 signal 之前,节点已经取消了
         */
        final boolean transferForSignal(Node node) {
            /*
             * 在这里将 waitStatus 置为 0.
             * 如果成功设置为0, 那么继续往下面执行
             * 如果CAS 失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取消,那么直接return false.
             */
            if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
                return false;
    
            /*
             * enq(node): 自旋进入阻塞队列的队尾.这个在将lock()方法的时候大家见到过.就是同一个方法.
             * 这里的返回值 p 是 node 在阻塞队列的前驱节点
             */
            Node p = enq(node);
            int ws = p.waitStatus;
            // ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。
            // 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用
            // 因为节点入队后,需要把前驱节点的状态设为 Node.SIGNAL(-1)
            if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
                // 如果前驱节点取消或者 CAS 失败,会进到这里唤醒线程(但是本场景下不会执行到这里)
                MyLockSupport.unpark(node.thread);
            // 返回true
            return true;
        }
    

     将上面这段代码总结一下就是: 将本节点的waitState设置为了0. 然后让本节点插入到到锁的`等待队列`, 然后将前驱节点的waitState设置为了1. 然后返回了true.  

    这一行的tansferForSignal返回了true, 取反了就是false了, 所以退出了 while循环. 至此signal方法就执行完毕了.

    signal干的主要事情就是: 把`条件队列`里的第一个元素转移(尾插)到了锁的`等待队列`里. 

    `条件队列`就是firstWaiter为头结点的一个链表.

    `等待队列`就是咱们上面将lock() unlock()的时候提到的锁的等待队列.

    signal方法执行完了后, 接下来就该执行unlock()方法了. 如下图:

    unlock()所做的事情就是, 释放当前的锁, 然后激活`等待队列`里的第一个线程. 

    而在本场景下, 现在等待队列里有且仅有一个元素, 就是signal方法转移的那个元素.

    unlock()之前分析过, unlock会调用release方法:

    release方法所做的就是释放锁(第一个红色代码), 然后唤醒`等待队列`里的第一个线程(第二个红色代码).

    unlock()方法执行完了后, 刚才await挂起的那个线程就又被激活了.

    所以接下来执行的是acquireQueued方法, 这个方法在将锁的时候讲过, 所以这里简单讲解一下:

    final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (; ; ) {
                    // 本场景下: node是队列里的第一个元素, 也就是await的线程对应的node.
                    // 本场景下: p是node的前一个节点, 也就是head节点了
                    final Node p = node.predecessor();
                    // 本场景下: p==head. 锁现在空闲, tryAcquire也会成功.
                    if (p == head && tryAcquire(arg)) {
                        // 将node设置为新的head. head节点隐含的意思就是: head节点对应的线程是当前锁的持有者
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
    
                        // 返回false. 因为本场景下该线程没有被中断过.
                        return interrupted;
                    }
                    
                    //--- 本场景下, 不会执行到下面的代码
                    
                    if (shouldParkAfterFailedAcquire(p, node)
                            && parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed) cancelAcquire(node);
            }
        }
    

     最终这个方法返回了true. 接下来, await()方法继续执行剩下的几行代码就可以退出了:

    这两行就是做了相应的维护操作, 和线程中断判断, 这里就不讲解了. 

    随后,await方法执行完了, 退出方法栈.

    然后就继续往下执行.  执行System.out.println, 然后是unlock.

    至此本段程序就执行完了. 

  • 相关阅读:
    2019-2020-3 20175324 王陈峤宇《网络对抗技术》Exp3 免杀原理与实践
    2019-2020-2 20175324 王陈峤宇《网络对抗技术》Exp2 后门原理与实践
    2019-2020-3 20175324王陈峤宇《网络对抗技术》Exp1 PC平台逆向破解
    2019-2020- 20175324王陈峤宇 Exp0 环境搭建-KALI LINUX安装
    2019-2020-2 20175330杨璟旭《网络对抗技术》Exp9 Web安全基础
    2019-2020-2 网络对抗技术 20175330 Exp8 Web基础
    2019-2020-2 网络对抗技术 20175330 Exp7 网络欺诈防范
    2019-2020-2 网络对抗技术 20175330杨璟旭 Exp6 MSF基础应用
    2019-2020-2 20175330杨璟旭《网络对抗技术》Exp5信息搜集与漏洞扫描
    2019-2020-2 20175330杨璟旭《网络对抗技术》Exp4 恶意代码分析
  • 原文地址:https://www.cnblogs.com/noKing/p/9367563.html
Copyright © 2011-2022 走看看