失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 详解ReentrantLock为什么是可重入锁

详解ReentrantLock为什么是可重入锁

时间:2019-09-01 20:49:17

相关推荐

详解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 -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 {// UnFairLocknew Thread(ReentrantLockNestedTest::testWithUnFairReentrantLock, "t1").start();} catch (Exception ex) {throw new RuntimeException(ex);}}}

测试结果如下图所示,由图可知,线程t1第一次获取锁,此时当前线程持有锁数量为1,即count=1,

执行到子方法时,线程t1,第二次获取锁,此时当前线程持有锁数量为2,即count=2,并且正常执行,

没有出现异常(死锁)。

4 为什么可重入

上面测试样例展示了同一线程多次获取锁的工作过程,下面解析ReentrantLock如何实现可重入的。

由源码可是可重入锁ReentrantLock有两种形式的锁:公平锁和非公平锁。

这里以非公平锁为例,阐述ReentrantLock的可重入特性。

4.1 可重入锁ReentrantLock

第一步是新建锁对象。由源码可知,ReentrantLock默认情况是非公平锁,即通过NonfairSync创建非公平锁同步对象。

java.util.concurrent.locks.ReentrantLock#ReentrantLock()

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)方法。

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看看如何实现。

4.3.1 tryAcquire

java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire

此时进入tryAqcuire,这里,封装可一个方法,nonfairTryAcquire,所以,要探究竟,要进入该方法。

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

当然了,释放锁的时候,也要逐步释放。

4.3.2 addWaiter

这是什么操作?

将线程放入队列。填充队列prev和tail,这里,填充了prev才能保证后面的方法acquireQueued返回true。

java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter

4.3.3 acquireQueued

进一步确认当前线程在队列,而不强行中断,保证线程正常执行。

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued

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)。

如果觉得《详解ReentrantLock为什么是可重入锁》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。