[源码分析]ReentrantLock & AbstractQueuedSynchronizer

Posted noking

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[源码分析]ReentrantLock & AbstractQueuedSynchronizer相关的知识,希望对你有一定的参考价值。

[源码分析]ReentrantLock & AbstractQueuedSynchronizer

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


一. sync字段

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

技术分享图片

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

二. lock/unlock依赖Sync

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

技术分享图片

技术分享图片

三. 锁内部类定义

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

技术分享图片

四. ReentrantLock构造器

ReentrantLock有两个构造器.

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

技术分享图片

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

技术分享图片

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

首先从最外层的调用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, 不会继续往下执行了. 当前线程获取到了锁, 而且执行了所有该执行的内容, 就完事儿了.

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

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

重入用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, 表示成功. 

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

cas争抢失败

场景如下:

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

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

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

技术分享图片

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

而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;
                }
            }
        }
    }

  

未完待续

以上是关于[源码分析]ReentrantLock & AbstractQueuedSynchronizer的主要内容,如果未能解决你的问题,请参考以下文章

concurrent互斥锁ReentrantLock & 源码分析

Java并发编程:ReentrantLock-NonfairSync源码逐行深度分析(中)

ReentrantLock源码分析

ReentrantLock源码分析--jdk1.8

ReentrantLock源码分析

ReentrantLock源码分析-JDK1.8