您的当前位置:首页正文

彻底弄懂ReentrantLock —— 超详细的原码分析

2024-11-07 来源:个人技术集锦

ReentrantLock 是基于 AQS 框架实现,JDK 中线程并发访问的同步技术,

它是一个互斥锁,也叫独占锁,支持可重入,支持锁的公平性。

大神Doug Lea写的,整个AQS框架都是他一个人撸出来的,牛!

Doug Lea有多牛

就像:

  • 谈喜剧电影,不可能不提星爷
  • 谈相声,绕不开郭德钢
  • 说中国教育,总得聊聊蔡元培
  • 说到心理学,一定得说弗洛伊德
  • 说到并发编程,Doug Lea不得不提

java1.6 之前, synchronized效率太低了,大神Doug Lea就开发了AQS框架,就是解决并发问题。

JDK的出品公司SUN公司,一看,呀,这AQS性能那是相当可以,好吧,那就吸收进来,直接放JDK里吧。

synchronized可是SUN公司亲生的呀,可性能太差,SUN公司琢磨,要不优化下synchronized

于是乎,SUN公司的开发团队,历经数年,优化了synchronized,其性能大大提升,和AQS不相上下了。

从某种程度上说,Doug Lea 以一已之力,PK SUN公司整个开发团队,牛人!

这里从ReentrantLock 加锁解锁机制,由浅入深,让你彻底弄懂其原理。

一、应用场景

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(1);
        for(int i = 0; i < 10; i++){
            new Thread(() -> {
                try {
                    downLatch.await();
                    for(int j = 0; j < 1000; j++){
                        total++;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        Thread.sleep(2000);
        downLatch.countDown();
        Thread.sleep(2000);
        System.out.println(total);
    }

这段代码,起10个线程,每个线程加 1000 次,期望值是 10000,但因为并发,最后会小于 10000


    public static void main(String[] args) throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(1);
        ReentrantLock lock = new ReentrantLock();
        for(int i = 0; i < 10; i++){
            new Thread(() -> {
                try {
                    downLatch.await();
                    lock.lock(); // 加锁
                    for(int j = 0; j < 1000; j++){
                        total++;
                    }
                    lock.unlock(); // 解锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        Thread.sleep(2000);
        downLatch.countDown();
        Thread.sleep(2000);
        System.out.println(total);
    }

控制了并发访问,算出来的结果就是 10000

二、什么是AQS

简单讲,AQS(AbstractQueuedSynchronizer),是大神Doug Lea写的,一个同步框架。

JDK 中 java.util.concurrent 包下,很多处理并发的工具类都直接或间接引用了AQS,比如ReentrantLock,Semaphore,CountDownLatch……

  • 首先说下,AQS 里面的内部类Node的几个属性

volatile int waitStatus;  // Node里,记录状态用的

volatile Thread thread;  // Node里,标识哪个线程

volatile Node prev;  // 前驱节点(这个Node的上一个是谁)

volatile Node next; // 后继节点(这个Node的个一个是谁)
  • AQS 本身的属性

private transient Thread exclusiveOwnerThread; // 标识拿到锁的是哪个线程

private transient volatile Node head; // 标识头节点

private transient volatile Node tail; // 标识尾节点

private volatile int state; // 同步状态,为0时,说明可以抢锁

这里画了张图,很直观,咱们照着图来说

当锁释放时,唤醒节点去抢锁。

这个由Node对象组成的队列,叫CLH队列,是一个先进先出的队列,双向指针。

三、ReentrantLock加锁逻辑

这里以非公平锁为例来说明,这里画了个简化的流程图,先有个印象

ReentrantLock这个类,并没有直接继承AQS,而在ReentrantLock有一个内部类,sync,它继承了AQS。

ReentrantLock lock = new ReentrantLock();
// 当执行这段代码时,就会创建一个AQS 对象,其status是0,具体原码如下

    public ReentrantLock() {
        sync = new NonfairSync();  // 默认是非公平锁
    }

lock.lock(); // 加锁

执行这行代码时,会涉及到 自旋、 CAS、 入队、 阻塞,下面照着原始说逻辑,具体先不展开。

// 非公平锁实现
     final void lock() {
         if (compareAndSetState(0, 1)) // 成功将state 由0改为1的线程,直接就可以拿到锁
             setExclusiveOwnerThread(Thread.currentThread());
         else
             acquire(1); // 没拿到锁,做特殊处理
     }

// 获取独占锁
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
        	selfInterrupt(); // 在线程上打一个阻断标志
        }    
    }

acquire() 是获取独占锁的核心方法,咱先把整体逻辑讲完,然后再对原码进行逐行分析。

tryAcquire(arg)
尝试获取锁,成功返回true,否则返回false

addWaiter(Node.EXCLUSIVE)
自旋的方式入队,直到成功入队为止,否则重试

acquireQueued(final Node node, int arg)
再次尝试获取锁,若成功则返回,失败了就阻塞线程。其中阻塞线程是调用了 LockSupport.park() 方法

四、ReentrantLock解锁逻辑


    public void unlock() {
        sync.release(1);
    }

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease(arg)
修改 state 属性,state = 0 时,即解锁成功

unparkSuccessor(h)
唤醒头节点,使其去抢锁,其中解除阻塞,是调用了LockSupport.unpark()方法

用本文开头的代码,来个第一阶段的总结

       
       lock.lock(); // 假设10个线程同时走到这行,只可能有一个线程拿到锁,继续往下走,其他线程阻塞,停在这一行。
       for(int j = 0; j < 1000; j++){
           total++;
       }
       lock.unlock(); // 执有锁的线程释放了锁,阻塞的线程有机会抢锁,
      				 // 抢锁成功的可以往下走,其余的继续阻塞。
       

在加锁解锁过程中,用了CAS,用了自旋,简要说下它俩啥意思,然后再开始讲原码。

CAS 可以保证,不论并发有多高,只可能有一个线程执行成功。


比如说,现在账户里有10块,现在要给账户上加5块钱,三个线程同时执行这个操作。

CAS要求传两个值过去 原来的值10,改动后的值15。底层执行的时候,先比较一下,当前值是不是10。

如果不是,就直接返回false,如果是10,那就将10改为15。

这里比较并修改是原子操作,底层语言保证这一点。

刚刚说的那三个线程,只有一个线程会修改账户的钱,并返回true,其它两个会返回失败,不修改账户的钱。

自旋 简而言之就是死循环,比如 while(true){},名字很高大上,其实就那么回事。

至此,ReentrantLock加锁解锁,最基本的知识讲完了。若还不太理解,从头现看,不要看下边的。

如果说是初次接触ReentrantLock,那就不要往下看了,那是会看吐的。听话,别看!

欢迎转载,码字不易,请注明出处:https://editor.csdn.net/md/?articleId=108818343。

五、加锁原码分析

先看下,加锁的流程图,有个具体的印象,再看源码

假设有ABCD四个线程,同时执行到加锁这行代码。前文说过,state是0,代表可抢锁。

        final void lock() {
            if (compareAndSetState(0, 1)) // 假设A执行此代码成功
                setExclusiveOwnerThread(Thread.currentThread()); // 标识哪个线程执有该锁
            else
                acquire(1);
        }

// 这里compareAndSetState(0, 1),就是进行CAS操作
    protected final boolean compareAndSetState(int expect, int update) {
        // unsafe类调用的是native方法
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
// 线程A拿到了锁就直接返回了,那线程B C D 就进入 acquire(1) 方法
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
             selfInterrupt();
        }
    }
  • 先说 tryAcquire(arg) 就是抢锁
		// tryAcquire 这个方法,非公平锁调用这个方法,传入参数是1,这个与可重入相关,待会再细说。
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); // 获取 state状态,
            if (c == 0) { // state是0,继续抢锁。刚进入lock 没抢到,现在可能锁已释放,有可能这次就抢成功了。
                if (compareAndSetState(0, acquires)) { // B C D 线程再次竞争拿锁
                    setExclusiveOwnerThread(current);
                    return true; // 拿到了锁,lock 方法就结束了。
                }
            }
            // 来抢锁的线程,本身就执有锁,说明是再次加锁,state 再加 1
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow 内在溢出了,抛出异常
                    throw new Error("Maximum lock count exceeded");
                setState(nextc); 
                return true;
            }
            return false; // 抢锁失败
        }
        

这个方法没有难理解的逻辑,不过,setState(nextc); 这个方法没有用 CAS,这里会不会是bug

明确的告诉你,不是,因为这里不存在并发

current == getExclusiveOwnerThread()

即当前线程是执有锁的线程,只可能有一个线程进入这人条件分支,不需要用CAS

这里就是ReentrantLock可重入的体现,state=0,指锁不属于任何线程,

当某线程首次抢到锁,state=1,

此线程未释放锁的情况下,再次抢到锁,state=2,

这种情况下,只有连续释放两次锁,其它线程才可能抢到该锁。

这就是 ReentrantLock 锁的可重入。
  • 详细说说 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

addWaiter(Node.EXCLUSIVE),入队,自旋能保证,一定入队成功

addWaiter(Node.EXCLUSIVE)  这个方法就是用自旋的方式,保证线程入队

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode); // 线程与Node绑定
        Node pred = tail;
        if (pred != null) { // 队列已经初始化,直接将new 出来的node 放到队尾
            node.prev = pred;
            if (compareAndSetTail(pred, node)) { // CAS 设置队尾
                pred.next = node;
                return node;
            }
        }
        // 走到这里,说明队列未初始化,或者上面并发入队,入队失败了。
        enq(node); 
        return node;
    }

	// 自旋方式入队
    private Node enq(final Node node) {
        for (;;) { // 死循环,保证入队一定成功
            Node t = tail;
            if (t == null) { // 队尾是null,说明得初始化队列
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

还记得上文画的那个AQS的图么

还是刚刚的4个线程,假设此刻 A 还没有释放锁,那state一定不等于0,exclusiveOwnerThread记录的就是线程A,

当B C D线程都进入addWaiter方法时,图中 head==null , tail == null,

想入队就要先初始化,即 t == null那个分支的代码。

如下图所示,这个就是头节点,thread==null,之后入队的,thread一定有值

那 C D 线程进入else分支,

它们俩个,谁执行compareAndSetTail()成功,谁就入队,

假设是C入队成功(如下图),那 B D 线程进入下一轮循环,

若不存在并发,就顺次入队。但最终大概都是这个样子。

这个方法里的逻辑是巨复杂的,不太好理解。

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); // 获取前驱节点
                if (p == head && tryAcquire(arg)) { // 是队列的第二个节点,并且抢锁成功
                    setHead(node);
                    p.next = null; // 从队列中剔除,等待GC 回收
                    failed = false;
                    return interrupted; // lock 方法结束
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
  	                  interrupted = true; 
                }
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

	// 设置头节点
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

先解释下这个条件 p == head && tryAcquire(arg) 前驱节点是head,且此节点抢锁成功,

结合上面那个图,B C D 三个节点,只有 C 节点才可以 抢锁,不要问为什么,这是规矩

B D 线程进入shouldParkAfterFailedAcquire(p, node)   parkAndCheckInterrupt() 这两上方法。

原来代表C线程的node成为新的头节点(抢锁成功,就要出队)。

head 指向新的头节点,头节点thread 设置为null,

新旧头节点之间的指针去掉,旧的头节点等待GC回收。

在讲解shouldParkAfterFailedAcquire(p, node)parkAndCheckInterrupt() 这两个方法之前,

说点题外话。


B C D 三个节点,只有 C 节点才可以 抢锁,Why ? 大神Doug Lea 就是这么写的,你想咋地!

个人认为,这样设计,代码实现简单,线程都已经进入等待队列了,说明并发比较高,

抢锁就派个代表出去就行了,别的继续在队列中等,出去抢锁的线程多了,CPU有意见。

再说,去的再多,也是只能是一个抢到锁。

派谁去,当然是头节点,去掉代码实现简单易懂,去其中任意一个,代码更加复杂。

不是说非公平锁么,那队列里,排在前面的先出队列去抢锁,很公平啊,哪里来的非公平?


这是个好问题,公平与非公平锁,不是在这里体现的。

不管公平锁还是非公平锁,队列里,都是前面那个出队抢锁,没区别,

公平与非公平,主要差别是tryAcquire() 这个方法上,

公平锁,若队列不为空,没入队的线程不得抢锁,

非公平锁,若队列不为空,没入队的线程却可以抢锁,

这时后到的线程可能先抢到锁,即不公平。

言归正传

  • shouldParkAfterFailedAcquire(p, node) 判断是否要阻塞线程

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 前驱节点的waitStatus
        if (ws == Node.SIGNAL) // Node.SIGNAL 表示 -1
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这段代码逻辑很复杂,一句话,前驱节点 waitStatus是-1,就返回true.

不过,今天讲 lock() unlock() ,waitStatus都是初始状态0。

之后的博客中,还会再次讲到这个方法。


结合上图,以线程 B为例  waitStatus状态都是 0,前驱节点是头节点,其waitStatus也是0

首次进入shouldParkAfterFailedAcquire方法,
执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL)这段代码之后,前驱节点waitStatus=-1,
之后走下一行 return false, compareAndSetWaitStatus 方法结束。
那在方法acquireQueued()for (;;) {},第一次循环就结束了, B 没拿到锁

for (;;) {} 第二次循环开始,再次抢锁,
假设还没拿到,会第二次进入shouldParkAfterFailedAcquire()
此时B 线程的前驱节点,waitStatus=-1 该方法返回 true;(解锁时会讲到这一点)
那程序会走到 parkAndCheckInterrupt()这个方法里,阻塞线程

也就是说,经过两次循环,才会去阻塞线程,每次循环都会去抢锁的

Doug Lea 设计的真是好,发现这个线程需要阻塞,还要再给次抢锁的机会。

万一抢到了呢!真的是抢不到,再真正阻塞线程。

  • parkAndCheckInterrupt() 阻塞线程
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 阻塞线程
        return Thread.interrupted(); // 清除线程中断标记
    }
题外话
	park()阻塞了线程,有两种途径可以唤醒该线程:1)被unpark()2)被interrupt()。
	
	Thread.interrupted()当且仅当 线程被阻断时返回true,它还会清除当前线程的中断标记位,

若要了解 unpark() interrupt() 唤醒有何不同,请参看我的另外一篇博客——

至此,lock 调用的内层代码讲完了,再回头看下抢锁的总逻辑

// 线程A拿到了锁就直接返回了,那线程B C D 就进入 acquire(1) 方法
    public final void acquire(int arg) {
    	// 抢锁,直到抢到为止,没抢到就在acquireQueued一直自旋转。
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
             selfInterrupt(); // 如果线程被中断过,给线程打个标记
        }
    }

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

上面这段代码很有意思,作为题外话说说,当然你可以不看,有点绕。最好别看,看了会看糊涂的。

想想这段代码 Thread.currentThread().interrupt(); 什么时候会执行?

只有tryAcquire(arg)返回 false 且 acquireQueued() 返回 true 的时候才可以。

再看下,acquireQueued()什么时候才会返回 true 呢? 咱再看下原码

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

接着上文说,B线程 在for (;;)中,第二次进入shouldParkAfterFailedAcquire(),返回true,
进入parkAndCheckInterrupt(),调用 LockSupport.park(this)后,
返回 return Thread.interrupted();   

而 Thread.interrupted() 结果是false,因为目前 B线程 未调用过 interrupt(),继续自旋,

若在自旋过程中,其它的代码调用了interrupt(),那么 parkAndCheckInterrupt()会返回 true,

interrupted = true; 会被执行。

那 B线程在自旋过程中 parkAndCheckInterrupt() 会清除掉中断标记,但 interrupted = true是不会。

最终在acquire() 方法里,会明确的知道拿到锁的线程,曾经是否被中断过,若中断过,会重新在线程上做标记。



还有一点要说的

线程B 在自旋过程中,第二次 for 循环,会调用LockSupport.park(this)方法,程序就暂停了,不会再占用CPU资源了。

当被unpark()interrupt()唤醒时,会接着自旋,要么拿到了锁,

要么重新调用LockSupport.park(this)方法,继续等待。

具体是被unpark()唤醒的,还是被interrupt()唤醒的,程序上做了区分。

区分不区分,lock(), unlock() 用不到,啥时候乃至,之后的博客会写。

好了,加锁的原码,已讲解完了。真的是很复杂,相对应的,解锁的原码简单多了。

六、解锁原码分析

先看图,加锁与解锁画在一起,直观感受下

    public void unlock() {
        sync.release(1);
    }

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease(int releases) 是修改state值的

        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);
            return free;
        }

这段代码比较容易理解,也不存在并发,

调用一次 unlock(),state 值减1,当state = 0,即解锁成功,清除线程。

也就是说,连续加锁三次,即调用了三次lock() 方法,state=3,

解锁也得是3次,否则锁不会被释放。

unparkSuccessor(Node node) 是唤醒节点的


	if (h != null && h.waitStatus != 0) // h == null 无等待队列,h.waitStatus == 0,说明后面没有阻塞的队列,即不需要唤醒。
	                unparkSuccessor(h);



   private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0); // 负值设置为0
        Node s = node.next; // 找到有效的后继节点
        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;
        }
        if (s != null)
            LockSupport.unpark(s.thread); // 将后继节点唤醒。
    }

	// 这段代码其实很有意思的,说点题外话
	shouldParkAfterFailedAcquire() 这个方法还记得不,
	要阻塞节点的前提是,该节点的前驱节点 waitStatus= -1
	
	unparkSuccessor 这个方法中,s 是后继节点,若是null,说明不存在等待队列,无需唤醒。

	for() 循环中代码,可保证,s 是最前面的那个排除节点,唤醒它抢锁,因为锁释放了。
	

七、公平锁的实现逻辑

前面介绍是以非公平锁为例子来说明的,公平锁实现与这类似。

先记住差别,前面也说过了,

公平锁,等待线程不为空,只有入队才可以抢锁

非公平锁,等待线程不为空,也可以抢锁。

现在看原码级别的实现


ReentrantLock lock = new ReentrantLock(true); // 传入参数 true 即是公平锁

        final void lock() { // 公平锁lock方法,直接调用acquire(), 非公平锁是先抢锁,失败用调用acquire()
            acquire(1);
        }

公平锁与非公平锁,调用的 actuire() 方法是一样的,不一样的是tryAcquire()

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

		// 公平锁tryAcquire()逻辑,与非公平锁区别是多了!hasQueuedPredecessors() 这个判断
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            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;
            }
            return false;
        }

公平锁与非公平锁,差别就一行代码 hasQueuedPredecessors() ,看下它的原码

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

这个方法很精炼,直接说逻辑,

  • 若等待队列为空,即未初始化,返回 false;
  • 若等待队列已初始化,哨兵结点没有后继结点,返回false;
  • 若哨兵结点有后继结点,后继结点的线程是当前线程,返回false;
  • 其它情况返回 true

返回false就是可以抢锁。

理解了这个之后,再看 tryAcquire()


            if (c == 0) { // state 是 0 ,可以抢锁
                if (!hasQueuedPredecessors() &&  // 等待队列中有线程在等待时,只有头节点的后继线程可抢锁,其它没资格。
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }

由此看出,tryAcquire() 方法保证了,抢锁时,若等待队列中有线程在等待,外来的线程就不能抢锁,只能先入队,即排在后面。

八、小结

欢迎转载,码字不易,请注明出处:https://editor.csdn.net/md/?articleId=108818343。

ReentrantLock 中 lock(), unlock() 方法的分析,至此结束,从中可以看出以下几点

  • ReentrantLock 可实现公平锁和非公平锁,其差别是,等待队列有线程等待时,抢锁逻辑不同
  • ReentrantLock 手动加锁,解锁。加锁解锁次数必需相等,否则锁不会被释放
  • ReentrantLock 可实现锁的重入,这与state数值有关

另外,ReentrantLock 锁有可中断的特性,本文没有涉及到,随后的博客会讲解这个。

Top