一文学会ReentranLock(重入锁)

611 阅读8分钟

什么是ReentrantLock

ReentrantLoock也称为可重入锁。可重入意味着已持有锁的线程,可以重复地获取锁,表现为,如果锁不支持重入,那么当持有锁的线程再次上锁时将会被阻塞住。如当在递归的方法里获取锁时,递归将因申请锁而被阻塞住。

锁的知识分两部分,一分部为如何加解锁,另一部分为把锁分配给谁。ReentrantLoock依赖于AQS,AQS解决了把锁分配给谁的问题,ReentrantLoock只需解决如何加解锁即可。

AQS原理可以参考一文了解AQS,了解AQS将使ReentrantLock变得简单,不了解也不妨碍理解。

ReentrantLock的特点为:

  • 可重入
  • 为独占锁,同一时间只有一个线程能获取到锁
  • 支持公平非公平的方式获取锁,公平意味着,按申请顺序锁的顺序分配锁
  • 继承了AQS的其他特性

ReentrantLock实现了Lock语义,Lock接口代表着锁所应具有的方法模板

public interface Lock {
    void lock(); // 获取锁,获取不到会被阻塞
    void lockInterruptibly() throws InterruptedException; // 获取锁,可被中断,获取不到会被阻塞
    boolean tryLock(); // 获取锁,无论结果如何不会被阻塞
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 获取锁,最多在unit时间内返回结果,可被中断
    void unlock(); // 释放锁
    Condition newCondition(); // 支持满足一定条件后,再去上锁
}

AQS基础

假设我们已经了解了加解锁的控制,现在先简要地了解AQS是如何分配锁的:

AQS运行概要.png

  1. 当申请锁,即调用了与acquire()类似语义的方法时,AQS将询问子类是否上锁成功,成功则继续运行。否则,AQS将以Node为粒度,记录这个申请锁的请求,将其插入自身维护的CLH队里中并挂起这个线程
  2. 在CLH队列中,只有最靠近头节点的未取消申请锁的节点,才有资格申请锁
  3. 当线程被唤醒时,会尝试获取锁,如果获取不到继续挂起;获取得到则继续运行
  4. 当一个线程释放锁,即调用release()类似语义的方法时,AQS将询问子类是否解锁成功,有锁可以分配,如果有,AQS从CLH队列中主动唤起合适的线程,过程为2、3
  5. 如果需要等待条件满足再去申请锁,即调用了wait()类似语义的方法时,在AQS中表现为,以Node为粒度,维护一个单向等待条件队列,把Node所代表的线程挂起
  6. 当条件满足时,即调用了signal()类似语义的方法时,唤醒等待条件队列最前面的未取消等待的Node,执行1
  7. 子类可以维护AQS的state属性来记录加解锁状态,AQS也提供了CAS的方法compareAndSetState()抢占更新state

简要来说,AQS分配锁时,当前线程可能会被挂起,接着被唤醒继续尝试申请锁,重复此过程直到获取到锁或取消等待。从外部看,就如入口方法被阻塞并在未来被恢复了一样。

Sync

现在,就可以来看看ReentrentLock是怎么回事。在ReentrentLock中,以内部类Sync继承AQS,之后要说到的公平锁不公平锁也是基于Sync的子类实现。Sync主要的方法为 nonfairTryAcquire() 和 tryRelease()。

        final boolean nonfairTryAcquire(int acquires) {
            // 当前线程
            final Thread current = Thread.currentThread();
            // 被加锁的次数
            int c = getState();
            if (c == 0) {
                // 说明没有线程获得锁
                if (compareAndSetState(0, acquires)) {
                    // 加锁次数通过CAS设置为acquires次
                    // 记住拿到锁的线程
                    setExclusiveOwnerThread(current);
                    // 加锁成功
                    return true;
                }
            }
        
            else if (current == getExclusiveOwnerThread()) {
                // 被加锁的次数不为0,说明被某个线程持有,进到这里说明重入,即同一线程再次加锁了
                // 要更新的加锁次数
                int nextc = c + acquires;
                // 加锁次数无论如何都不会小于0,如果小于0,说明发生了并发错误
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 更新加锁次数
                setState(nextc);
                // 加锁成功
                return true;
            }
            // 加锁失败
            return false;
        }

nonfairTryAcquire()以不公平的方式获取锁,当持有锁的线程再次申请锁时,可以再次上锁,发生了重入。值得说明的是,在此方法中通过setState()更新上锁次数是,不需要考虑并发性,因为这是发生在同一线程的连续行为。

        protected final boolean tryRelease(int releases) {
            // 释放锁后,要跟新的加锁次数
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                // 非申请锁的线程,不能释放锁
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                // 说明释放后,没有锁被线程持有了
                // 如果没有进入到这里,说明还有锁被当前的线程持有,之前有重入
                free = true;
                // 没有拿到锁的线程了
                setExclusiveOwnerThread(null);
            }
            // 更新了上锁的次数
            setState(c);
            // true代表告诉AQS有锁可以继续分配了,false表示告诉AQS没有锁可以分配
            return free;
        }

tryRelease()为AQC要求子类实现的,询问是否解锁成功并有锁可以继续进行分配的方法。与前面nonfairTryAcquire()相同,不需要考虑并发,只有加锁的线程才会调用到。

其他的方法简要说明为:

  • lock(): 子类要实现的加锁入口方法
  • isHeldExclusively():检查锁的持有线程是否是当前线程
  • newCondition():返回AQS支持的,允许线程等待条件再申请锁的入口操作对象
  • getOwner():返回当前持有锁的线程
  • getHoldCount():被持有的锁数量
  • isLocked():是否有线程持有锁
  • readObject():从序列化中恢复Sync

ReentranLock默认为不公平锁,可以通过实例化方法参数,决定使用公平锁或不公平锁,其实现均为Sync的子类。

不公平锁的实现

不公平锁的实现就比较简单了,Sync已经支持nonfairTryAcquire()以不公平的方式支持获取锁,那么代表不公平锁的实现NonfairSync只需要维持AQS首次请求分配锁时不公平的特点即可(AQS基础一节图中首次请求锁没有要求入队等待,所以有可能插队)。

    static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                // ① 直接上锁成功,不用从AQS中等待了
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 从AQS中请求所分配
                acquire(1);
        }

        // 这个方法是AQS要求子类实现,并向子类寻求加锁的方法
        protected final boolean tryAcquire(int acquires) {
            // 直接调用了Sync的方法
            return nonfairTryAcquire(acquires);
        }
    }

tryAcquire()为AQS要求子类实现的,并向其询问是否成功加锁的方法。①中,如果能上锁,就不麻烦AQS去分配锁了,上锁状态也通过CAS保存了。那么,这里是可以插队的。设想某一时刻,AQS中有在队列中等待分配锁的代表线程的Node,此时有线程释放了锁,然后时间片分配给了刚刚调用到lock()的线程,那么这个线程拿到了锁,插了队。同样的情况通过AQS的acquire()也有机会发生。

公平锁的实现

不公平锁由Sync的子类FairSync实现。

    static final class FairSync extends Sync {

        final void lock() {
            acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            // 当前线程
            final Thread current = Thread.currentThread();
            // 加锁的次数
            int c = getState();
            if (c == 0) {
                // ① 没有线程持有锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 进到这里说明,1.队列中没有等待分配锁的线程 2.获取到了锁,因为成功通过CAS更新了加锁次数
                    setExclusiveOwnerThread(current);
                    // 告诉AQS获取到了锁
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                // 说明当前线程重入
                // 要更新的上锁次数
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 更新锁次数
                setState(nextc);
                // 告诉AQS加锁成功
                return true;
            }
            // 告诉AQS加锁失败
            return false;
        }
    }

在①中,FairSync处理了插队的情况。当FairSync通过acquire()让AQS分配锁时,虽然AQS会先通过tryAcquire()让FairSync上锁,不成功再把申请锁的线程加入队列。但是FairSync需要保持公平地特性,那么通过检查hasQueuedPredecessors()发现有代表线程的Node在等待锁时,直接返回加锁不成功,杜绝了插队。

总结

Lock接口的其他方法语义,ReentranLock未作封装直接使用了AQS的特性就可实现,因此不做展开。对与ReentranLock的使用,按照Lock的接口语义使用即可,最简单的加锁解锁方法分别为Lock.lock()、Lock.unLock()。

那么,ReentranLock的意义为,拓展了AQS,实现了可重入的,公平或不公平的特性。

  • 可重入的实现为:当AQS向它的子类Sync(的子类)申请加锁发生失败时,Sync将判断申请锁的线程是否为持有锁的线程,如果是则增加上锁的次数,允许通过,也就发生了重入。那么,重入产生的多余的上锁次数,也只有上锁的线程能释放。
  • 公平或不公平的实现为:当向AQS申请分配锁时,AQS向它的子类Sync(的子类)申请加锁,如果失败,就将申请锁的请求放到CLH队列中排队。那么,Sync就可以通过发现CLH队里中还有线程排队时,直接告诉AQS加锁是否失败,决定了锁是否公平。于NonfairSync,它忽略了AQS中队列的情况直接尝试插队;于FairSync,它发现队列还有等待的代表申请锁的线程的Node时,不直接加锁。

综上,ReentranLock并没有过多的特别操作,也没有属于自己的生硬的技术点,也就是说理解ReentranLock的程度,取决于理解AQS的程度。不仅仅是ReentranLock,Java中的许多锁也都依赖于AQS。因此,仅学习ReentranLock还不够,还需要进一步拿下AQS,才能一窥Java众多并发的核心秘密。

ASQ学习传送门