有一次,公司有人在面试,路过时,听到面试官问到了锁,
让面试者聊一聊用到的锁,
我此时,也是心里一震,
我用过哪些锁?为什么使用?
搜索了好一会儿,哈哈哈,我就是这么菜。
只学习过synchronized、CountDownLatch,没有其他储备。
心想,如果我当时是面试者,该多没脸,直接没有了机会。
我该怎么办?学呗。
那么,就有了这个可重入锁的详解。
可重入,即一个线程可以多次(重复)进入同类型的锁而不出现异常(死锁),这里的死锁:自己等待自己释放再获取,所以无限循环。
如:同一线程,执行逻辑中,嵌套获执行多个synchronized代码块或者嵌套执行ReentrantLock代码块。
在解析为什么同一线程可以重入(多次持有锁)之前,先给出一个测试样例。
该样例展示了同一线程多次进入同一个锁对象,而正常执行的过程。
执行过程是:线程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如何实现可重入的。
由源码可是可重入锁ReentrantLock有两种形式的锁:公平锁和非公平锁。
这里以非公平锁为例,阐述ReentrantLock的可重入特性。
第一步是新建锁对象。由源码可知,ReentrantLock默认情况是非公平锁,即通过NonfairSync创建非公平锁同步对象。
java.util.concurrent.locks.ReentrantLock#ReentrantLock()
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)方法。
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
acquire重入逻辑的入口。
在acquire中有两个过程,即tryAcquire和acquireQueued。
其中,
(1)tryAcquire是可重入的入口,实现当前线程的锁持有数自增,并且返回true,保证不会触发线程中断selfInterrupt();
(2)acquireQueued则是轮询获取已在队列中的线程,进一步判断,同一线程是否在队列,保证不会中断当前线程。返回false,保证不会触发线程中断selfInterrupt()。不过获取前,有一个入队的操作:addWaiter。
因为判断的逻辑为:if(!tryAcquire(…)&&acquireQueued(…)),所以,返回true,不会触发。
好了,下面需要进入tryAcquire看看如何实现。
java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
此时进入tryAqcuire,这里,封装可一个方法,nonfairTryAcquire,所以,要探究竟,要进入该方法。
java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
使当前线程持有锁的次数+1,保证逻辑正常执行。
进入该方法,可知,首先获取state,因为第一次获取锁后,state=1,
此时,会进入else if的逻辑,同一线程,则保证:current == getExclusiveOwnerThread()为true,
所以,此时,会将当前线程持有锁的次数+1,即next = c + acquires
当然了,释放锁的时候,也要逐步释放。
这是什么操作?
将线程放入队列。填充队列prev和tail,这里,填充了prev才能保证后面的方法acquireQueued返回true。
java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
进一步确认当前线程在队列,而不强行中断,保证线程正常执行。
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued