详解ReentrantLock为什么是可重入锁

1 缘起

有一次,公司有人在面试,路过时,听到面试官问到了锁,
让面试者聊一聊用到的锁,
我此时,也是心里一震,
我用过哪些锁?为什么使用?
搜索了好一会儿,哈哈哈,我就是这么菜。
只学习过synchronized、CountDownLatch,没有其他储备。
心想,如果我当时是面试者,该多没脸,直接没有了机会。
我该怎么办?学呗。
那么,就有了这个可重入锁的详解。

2 可重入锁

2.1 什么是可重入锁

可重入,即一个线程可以多次(重复)进入同类型的锁而不出现异常(死锁),这里的死锁:自己等待自己释放再获取,所以无限循环。
如:同一线程,执行逻辑中,嵌套获执行多个synchronized代码块或者嵌套执行ReentrantLock代码块。

2.2 为什么要用可重入锁

  • 最大程度避免死锁。

3 先给个测试例子,再源码解析

在解析为什么同一线程可以重入(多次持有锁)之前,先给出一个测试样例。
该样例展示了同一线程多次进入同一个锁对象,而正常执行的过程。
执行过程是:线程t1->获取主方法testWithUnFairReentrantLock()的锁->主方法中获取子方法testSubMethodWithUnFairReentrantLock()的锁,实现嵌套获取锁。

package com.monkey.java_study.lock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock测试.
 *
 * @author xindaqi
 * @date 2022-04-20 18:00
 */
public class ReentrantLockNestedTest {

    private static final Logger logger = LoggerFactory.getLogger(ReentrantLockNestedTest.class);

    private static final ReentrantLock unFairLock = new ReentrantLock();

    /**
     * 非公平锁测试.
     */
    public static void testWithUnFairReentrantLock() {
        try {
            logger.info(">>>>>>>>>>竞争锁,Thread name:{}", Thread.currentThread().getName());
            unFairLock.lock();
            int count = unFairLock.getHoldCount();
            int queue = unFairLock.getQueueLength();
            logger.info(">>>>>>>>>>竞得锁,执行任务,Thread name:{}, count:{}, queue:{}", Thread.currentThread().getName(), count, queue);
            Thread.sleep(2000);
            logger.info(">>>>>>>>>>执行完成:{}, count:{}, queue:{}", Thread.currentThread().getName(), count, queue);
            testSubMethodWithUnFairReentrantLock();
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            logger.info(">>>>>>>>>>释放锁:{}", Thread.currentThread().getName());
            unFairLock.unlock();
        }
    }

    /**
     * 子方法,测试可重入.
     */
    public static void testSubMethodWithUnFairReentrantLock() {
        try {
            logger.info(">>>>>>>>>>Sub method竞争锁,Thread name:{}", Thread.currentThread().getName());
            unFairLock.lock();
            int count = unFairLock.getHoldCount();
            int queue = unFairLock.getQueueLength();
            logger.info(">>>>>>>>>>Sub method竞得锁,执行任务,Thread name:{}, count:{}, queue:{}", Thread.currentThread().getName(), count, queue);
            Thread.sleep(2000);
            logger.info(">>>>>>>>>>Sub method执行完成:{}, count:{}, queue:{}", Thread.currentThread().getName(), count, queue);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            logger.info(">>>>>>>>>>Sub method释放锁:{}", Thread.currentThread().getName());
            unFairLock.unlock();
        }
    }

    public static void main(String[] args) throws Exception {
        try {
            // UnFairLock
            new Thread(ReentrantLockNestedTest::testWithUnFairReentrantLock, "t1").start();
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

测试结果如下图所示,由图可知,线程t1第一次获取锁,此时当前线程持有锁数量为1,即count=1,
执行到子方法时,线程t1,第二次获取锁,此时当前线程持有锁数量为2,即count=2,并且正常执行,
没有出现异常(死锁)。

详解ReentrantLock为什么是可重入锁_第1张图片

4 为什么可重入

上面测试样例展示了同一线程多次获取锁的工作过程,下面解析ReentrantLock如何实现可重入的。
由源码可是可重入锁ReentrantLock有两种形式的锁:公平锁和非公平锁。
这里以非公平锁为例,阐述ReentrantLock的可重入特性。

4.1 可重入锁ReentrantLock

第一步是新建锁对象。由源码可知,ReentrantLock默认情况是非公平锁,即通过NonfairSync创建非公平锁同步对象。
java.util.concurrent.locks.ReentrantLock#ReentrantLock()
详解ReentrantLock为什么是可重入锁_第2张图片

4.2 非公平锁NonfairSync

java.util.concurrent.locks.ReentrantLock.NonfairSync
通过NonfairSync创建非公平锁对象,NonfairSync继承Sync,实现lock方法,这个lock方法就是可重入锁实现可重入的入口。
由这个lock方法可知,先进入的逻辑是CAS,如果CAS失败,则执行acquire(1),这个acquire是可重入的入口,即通过acquire实现可重入。
重入过程:
(1)线程t1,第一次获取锁x.lock(),CAS成功,嵌套调用,第二次获取锁x.lock()时,CAS失败。
(2)第二次CAS失败后,会执行acquire(1),实现重入。忽略线程中断,并使当前线程持有锁的次数+1,保证逻辑正常执行。
为什么第二次CAS失败:因为第一次已经CAS了,内存数据已经发生了改变,变为1,所以第二次CAS(0,1)时,0≠1,因此失败。
接下来需要解析acquire(1)方法。
详解ReentrantLock为什么是可重入锁_第3张图片

4.3 acquire

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
acquire重入逻辑的入口。
在acquire中有两个过程,即tryAcquire和acquireQueued。
其中,
(1)tryAcquire是可重入的入口,实现当前线程的锁持有数自增,并且返回true,保证不会触发线程中断selfInterrupt();
(2)acquireQueued则是轮询获取已在队列中的线程,进一步判断,同一线程是否在队列,保证不会中断当前线程。返回false,保证不会触发线程中断selfInterrupt()。不过获取前,有一个入队的操作:addWaiter。
因为判断的逻辑为:if(!tryAcquire(…)&&acquireQueued(…)),所以,返回true,不会触发。
好了,下面需要进入tryAcquire看看如何实现。

详解ReentrantLock为什么是可重入锁_第4张图片

4.3.1 tryAcquire

java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
此时进入tryAqcuire,这里,封装可一个方法,nonfairTryAcquire,所以,要探究竟,要进入该方法。
详解ReentrantLock为什么是可重入锁_第5张图片

4.3.1.1 nonFairTryAcquire

java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
使当前线程持有锁的次数+1,保证逻辑正常执行。
进入该方法,可知,首先获取state,因为第一次获取锁后,state=1,
此时,会进入else if的逻辑,同一线程,则保证:current == getExclusiveOwnerThread()为true,
所以,此时,会将当前线程持有锁的次数+1,即next = c + acquires
当然了,释放锁的时候,也要逐步释放。
详解ReentrantLock为什么是可重入锁_第6张图片

4.3.2 addWaiter

这是什么操作?
将线程放入队列。填充队列prev和tail,这里,填充了prev才能保证后面的方法acquireQueued返回true。
java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
详解ReentrantLock为什么是可重入锁_第7张图片

4.3.3 acquireQueued

进一步确认当前线程在队列,而不强行中断,保证线程正常执行。
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued
详解ReentrantLock为什么是可重入锁_第8张图片

5 小结

  • 可重入,即一个线程可以多次(重复)进入同类型的锁而不出现异常(死锁);
  • ReentrantLock提供两类锁:公平锁和非公平锁;
  • 可重入是因为可重锁lock中核心逻辑:如果CAS,成功,则继续执行设置独占,setExclusiveOwnerThread;CAS失败,进入可重入逻辑;
  • 可重入执行逻辑入口:acquire(…),java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
  • 可重入的核心三个操作:
    • (1)addWaiter:线程入队;
    • (2)acquireQueued:确认线程在队列中,不中断线程,返回false;
    • (3)tryAcquire:同一线程获取锁次数+1,不中断线程,返回true;
  • 保证线程不中断:if(!tryAcquire(…)&&acquireQueued)。

你可能感兴趣的:(#,Java,ABC,可重入锁,ReentrantLock)