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为什么是可重入锁》对你有帮助,请点赞、收藏,并留下你的观点哦!