并发知识-- ReentrantLock

本文最后更新于:4 个月前

ReentrantLock 初步设计思想

new 一个 ReentrantLock 之后,调用 lock 方法,改变锁的状态码,从0变为1,主线程获取到锁,然后继续执行

如果此时同一个线程多次调用则进行多次重入,状态码增加

如果是线程交替执行,即并发度不是很高,那么就不存在锁的竞争,可以依次获取锁,然后依次释放,循环往复,保证高性能高可用

但是如果并发度很大,那么就一定存在线程之前对锁的竞争,这时,如果是公平锁,那么先获取锁的线程先处理,后获取锁的排在一个等待队列,然后这个线程就 park 暂停,然后等待锁释放和通知,unpark 后,再去获取锁

(注意:此处的 park 和 unpark 只是一种抽象概念,不一定指 LockSupport 里面的方法,也可以是 interrupt 中断)

但是,这里虽然想的很容易理解,但是要实现却不是那么容易,且看我如下分析

ReentrantLock 类数据结构

ReentrantLock 抽象内部类 Sync 锁

图 ReentrantLockSyncAndExtend:

ReentrantLockSyncAndExtend

图 ReentrantLockSyncAndExtend1:
ReentrantLockSyncAndExtend1

从源码和上图可知, Sync 只有第一个 lock() 方法是抽象的,有一个默认的非公平的 nonFairTryAcquired(int) 方法,这个方法是尝试获取锁的,并且是一种非公平的方式

Sync 的公平锁具体实现

图 AQSAbstractTryAcquire:

ReentrantLockFairSync

一个 lock 方法,一个 tryAcquire 方法

lock 方法是 Sync 的具体实现,而 tryAcquire 则是重写了 Sync 的父类 AQS 的方法

图 ReentrantLockFairSync1
ReentrantLockFairSync1

ReentrantLock 加锁过程 (公平锁)

第一次加锁

  1. new 是默认非公平锁(这里分析公平锁)

    sync = new FairSync();
  2. 当程序中调用 reentrantLock.lock() 方法时

    lock() 方法就会调用 sync.lock() 方法

    (sync 锁具体实现 lock 方法的 非公平的锁和公平锁不同)

    由上面两张图可知,在 lock() 方法里面直接调用的是 AQSacquire(int) 方法, 并且参数为1

  3. 那我们再去看 AQS

    AQSAcquire

    我们可以看到首先会调用 tryAcquire(int) 方法,这也是 AQS 中的一个方法,我们可以根据名称猜测这个方法是用来尝试获取锁

    点进去,发现:

    AQSAbstractTryAcquire

    仅仅抛一个异常?!

    这当然不可能,因为这样没有任何意义

    再按住 ctrlalt,鼠标点击 tryAcquire,出现如下具体实现:

    AQSAbstractTryAcquire2

    我们点击第一个,发现回到了 ReentrantLock 里面的 FairSync 静态内部类的 tryAcquire(int) 方法, 见图 ReentrantLockFairSync1

    这说明,内部的公平锁自己重写了 AQS 的方法但是没有加 @Override 注释!

  4. 再回到 ReentrantLock 看这个公平锁的 tryAcquired(int) 方法(和图ReentrantLockFairSync1 一样,只是把代码贴出来了)

    具体分析见注释:

    @Override
    protected final boolean tryAcquire(int acquires) {
        // 拿到当前线程
        final Thread current = Thread.currentThread();
        // 拿到 AQS 的内部 volatile 变量 state
        int c = getState();
        // 如果 state =0 说明此时没有线程拿到锁
        if (c == 0) {
            // 第一个 hasQueuedPredecessors() 是 AQS 的内部方法,是用来判断自己需不需要排队
            // 返回 false 在表示队列不存在或者队列中没有前驱(即队列中没有已经拿到锁的线程)
            // 那么这个线程就不需要排队
            // 为什么需要排队呢,最前面将思想的时候其实已经说了:
            // 想想如果存在多线程竞争,而且又是公平锁,那么就需要排队等待,直到前面那个线程处理完了,后面的线程才能拿到锁,再去处理
            // todo 这里理解排队要看之后的线程竞争或者其他特殊情况,我们这里假设的是第一个线程来拿锁,暂时不考虑,因为从开始到现在根本没有初始化队列(new FairSync 也没有初始化 Sync 父类的 AQS 队列)
            if (!hasQueuedPredecessors() &&
                    // 下面调用 AQS 的 CAS 方法,原子操作,改变的是 AQS 内部的 state 变量
                    // CAS 尝试加锁
                    compareAndSetState(0, acquires)) {
                // 如果前面的条件成立,说明: 1. 自己不需要排队 2. 自己成功拿到锁了
                // 此时就可以将锁设置为当前线程
                // 注意,这个方法是 `AQS` 继承 `AOS` 这个抽象类自己内部的方法,表示是一个排他锁
                // 关于 AOS 具体看下面相应的地方,这里就不多做解释,现在就认为是一个排他锁
                setExclusiveOwnerThread(current); 
                // 返回 true,表示成功加上了排他锁
                // 第一个线程到这里返回
                return true;
            }
        // 又有一个线程来了,如果锁状态不为0,说明已经加锁,并且该线程就是之前加锁的第一个线程,此时会重入,state +1,并返回 true,代表重入锁加锁成功
        } else if (current == getExclusiveOwnerThread()) {
            // 可能在这里会想,c 是最前面 getState() 方法得到的,为什么这里不用 CAS 来保证原子操作呢?
            // 因为其它的线程如果来了,会首先排队,不会改变第一个线程的状态值
            // 但是如果第一个线程此时刚好释放所了,岂不是重入错了?
            // 这里就要看上面的 else-if 判断了,如果是同一线程重入,然后释放,然后再加锁是不可能的,一个线程怎么可能同时加锁和解锁呢!
            // 上面是目前我的分析,如果加上锁自动释放(超时释放)的功能 tryLock 可能会有问题?
            // 解答: tryLock 是在指定时间内获取锁,而不是在指定时间内释放拿到的锁,拿到锁释放必须由这个线程显式调用 unLock 方法,所以根本不会存在自动释放锁这个逻辑
            // 超时释放指的是超过时间没有拿到就放弃抢锁,不是超时了就释放锁
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 如果锁状态不为0,并且是另一个线程来加锁,会返回 false
        return false;
    }

    关于 hasQueuedPredecessors() 方法,放到 AQS 章节再讲,这些 AQS 内部方法也有很多需要分析

  5. 方法 tryAcquire(int) 返回 true,回到前面 AQS 的 acquire

    此时 if 条件不成立,直接返回了,然后递归返回,就没有什么,结束程序中的 reentrantLock.lock() 方法,加锁成功,主程序继续执行

如果此时第一个线程又来加锁(重入)

此时在 ReentrantLock 中的 FairSynctryAcquire(int) 方法里面,进入 else 判断,请看代码注释 else-if 部分

tryAcquire(int) 返回 true, 然后也是和第一次拿到锁一样,调用递归返回,主程序继续执行

不过需要注意,FairSync 公平锁的父类 AQS 的内部状态 volatile 变量 state 每次都要加一,而每次释放锁的时候,state 才会减一,最后一定要变为0才释放了所有锁

如果是第二个线程来了,也开始加锁,并且此时第一个线程还没有释放锁

  1. 同上面两种情况,会调用 sync.lock() 方法,然后 acquire(1),再然后 tryAcquire(1)

    但是此时 if 和 else 条件都不满足,会返回 false

  2. 此时AQS 的 acquire(1) 方法中 if 的第一个 tryAcquire(1) 取反,值为 true,进入 acquiredQueue(addWaiter(Node.Exclusive,1)) 两个方法

    这两个方法就不在这里分析了,因为这是属于 AQS 内部实现,而且很复杂,需要单独说

    我们现在就只需要知道,这里第二个线程就会去在 AQS 内部排队等待

    再然后,会调用 selfInterrupt() 方法,这个 AQS 方法比较简单,就把代码截出来了

    AQSSelfInterrupt

    把当前线程中断,来让出 CPU 等待 AQS 的唤醒

ReentrantLock 加锁过程 (非公平锁)

如图 ReentrantLockNonFairSync:

ReentrantLockNonFairSync

第一次加锁

  1. 有源码和上图知,调用 ReentrantLock.lock() 方法时,会调用上图中非公平锁 NonFairSync 的方法

    1. 首先会 CAS 直接尝试抢占锁,如果抢到了就直接上锁,程序正常返回

      就是比公平锁少了一个判断方法 !hasQueuedPredecessors() 来判断需不需要排队

      非公平锁当然不需要排队啦~

    2. 如果没有没有直接抢到,那么其实还是要排队,调用 AQS 的 acquire() 方法

      注意,这个时候就不要认为之后的逻辑和公平锁一模一样,因为锁在继承 AQS 的实现的时候,都重写了 AQStryAcquire() 方法

      对于非公平锁来说,它默认使用的是 ReentrantLock 类中 Sync 父类里面的 nonFairTryAcquire() 方法

      1. nonFairTryAcquire()

        ReentrantLockNonFairTryAcquire

        和公平锁的实现没有太大差别,仅仅只是少了 hasQueuedPredecessor() 这个重要的查看队列的方法,会上来就去尝试获取锁,而不考虑是否存在等待队列

重入情况则和公平锁一致

同上的 else-if

第二个线程来加锁了

假设存在线程竞争,那么根据 ,和公平锁一样,还是会进入上图的 nonFairTryAcquire(),然后返回 false,然后调用 AQS 的 addWaiter()acquireQueued() 加入到等待队列中,selfInterrupt()方法自我中断

ReentrantLock 解锁过程

线程调用了 unLock

ReentrantLockSyncUnLock

然而这个 release(1) 方法又是 AQS 内部的方法:

AQSRelease

从上面可以看到,这里首先调用了一个 tryRelease(1) 方法

ReentrantLockTryRelease

  1. tryRelease() 方法

    这个是 ReentrantLock 内部 Sync 自己实现的默认方法,并且公平锁和非公平锁调用一致

    由于锁可以重入,所以这里 getState() 获取到的状态值可能大于1,所以此处要减去参数 release (=1) 拿到预期值

    然后进行判断,如果不是锁持有的线程拿到锁会抛出异常(一般不会出现)

    如果上面预期的状态值等于0,则说明锁可以释放,此时就把共享锁设为空;反之,不进行该操作

    (注意:锁为空并不代表锁已经释放,因为锁的状态是由 state 字段标识的,此时并没有改锁的状态)

    然后改变锁的状态 setState(),并且返回释放锁的结果

    想想这里改变状态为什么不用 CAS 操作?原因其实上面提到一些

    因为此时线程又不可能重入(这个线程正在释放,怎么可能会重入),如果是别的线程,在 setState() 操作之前,锁还没有释放又不可能来抢锁,所以这里就可以安安心心的进行锁的状态改变了

  2. tryRelease() 方法返回了,之后就是 AQS 的内部实现和状态判断了

    老规律还是把这个实现留在讲 AQS 的文章里面集中分析吧

    我们只需要知道此时锁已经释放,并且把 AQS 的内部状态 state 重置成 0,并且锁头已经置为空就行了

写在后面

AQS 和 ReentrantLock 代码,首先看子路老师的视频和马士兵的视频,然后对着源码一点点分析,做笔记,再结合博客看

自己再把 AQS 和 ReentrantLock 的类图先理清楚,有哪些内部类,哪些方法,哪些是抽象的,哪些是具体实现,分别做了什么,一层层理清楚

这篇文章仅仅分析了 ReentrantLock, 很多更重要的代码其实在 AQS 里面,具体分析就暂时等待 AQS 的文章吧

要不然,ReentrantLock 用到了 AQS 的一部分 api,我们就去看 AQS, 看了 AQS 又不知道像 tryAcquire()acquire() 到底分别在什么情况下使用,又要回到 ReentrantLock 去看 api 的应用,让人云里雾里,非常头疼

其实也没有分析很多,这么一想其实 ReentrantLock 还挺简单的