可重入锁-面试题:synchronized是可重入锁吗?
前言
面试题:synchronized是可重入锁吗?
答案:synchronized是可重入锁。ReentrantLock也是的。
1、什么是可重入锁呢?
关于什么是可重入锁,我们先来看一段维基百科的定义。
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
通俗来说:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
再换句话说:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
2、自己写代码验证下可重入和不可重入
我们启动一个线程t1,调用addOne()方法来执行加1操作。在addOne方法里面t1会获得rtl锁,然后调用get()方法,在get()方法里再次请求获取trl锁。
因为最终能打印value=1,说明t1在第二次获取锁的时候并没有阻塞。说明ReentrantLock是可重入锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantTest {
private final Lock rtl = new ReentrantLock();
int value = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantTest test = new ReentrantTest();
// 新建一个线程 进行加1操作
Thread t1 = new Thread(() -> test.addOne());
t1.start();
// main线程等待t1线程执行完
t1.join();
System.out.println(test.value);
}
public int get() {
// 获取锁
rtl.lock();
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get();
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
换成synchronized的加锁方式,同样能打印value的值。证明synchronized也是可重入锁。
public class ReentrantTest {
private final Object object = new Object();
int value = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantTest test = new ReentrantTest();
// 新建一个线程 进行加1操作
Thread t1 = new Thread(() -> test.addOne());
t1.start();
t1.join();
System.out.println(test.value);
}
public int get() {
// 再此获取锁
synchronized (object) {
return value;
}
}
public void addOne() {
// 获取锁
synchronized (object) {
value = 1 + get();
}
}
}
3、自己如何实现一个可重入和不可重入锁呢
不可重入:
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
可重入:
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock() throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
从代码实现来看,可重入锁增加了两个状态,锁的计数器和被锁的线程,实现基本上和不可重入的实现一样,如果不同的线程进来,这个锁是没有问题的,但是如果进行递归计算的时候,如果加锁,不可重入锁就会出现死锁的问题。
4、ReentrantLock如何实现可重入的
使用ReentrantLock你要知道:
ReentrantLock支持公平和非公平2种创建方式,默认创建的是非公平模式的锁。
看下它的构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
看下非公平锁,它是继承抽象类Sync的:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
看下公平锁,它也是继承抽象类Sync的:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
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;
}
}
NonfairSync、FairSync 和抽象类Sync 都是ReentrantLock的内部类。
Sync的定义,它是继承AbstractQueuedSynchronizer的,AbstractQueuedSynchronizer既是我们常说的AQS(后面我也会整理一篇)
abstract static class Sync extends AbstractQueuedSynchronizer {
}
好了,继承关系清楚了 ,现在我们看下ReentrantLock是如何实现可重入的
我们在addOne()和get()两个方法加锁的地方都打上断点。然后开始调式:
-
addOne方法获取锁的时候走到NonfairSync的“compareAndSetState(0, 1)”,通过CAS设置state的值为1,调用成功,并设置当前锁被持有的线程为当前线程t1;
image.png
-
继续调试,get方法获取锁的时候走到NonfairSync的“compareAndSetState(0, 1)”,通过CAS设置state的值为1,调用失败(因为已经被当前线程t1锁占有),走到else里面,继续往里看。走到NonfairSync的tryAcquire方法,再往里走;
image.png
-
会调用Sync抽象类里面的nonfairTryAcquire方法。源码解释我都写在下面了。
final boolean nonfairTryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// state变量的值
int c = getState();
// 因为c当前值为1,所以走else里面
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 判断当前线程 是不是 当前锁被持有的线程 ,判断为 true
else if (current == getExclusiveOwnerThread()) {
// c + acquires = 1 + 1 = 2
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);// 将state的值赋值为2
return true;
}
return false;
}
到此,可重入锁加锁的过程分析完毕。解锁的过程一样,希望你能自己debug下【调用的是Sync抽象类里面的tryRelease方法】
我这里总结一下:
-
当线程尝试获取锁时,可重入锁先尝试获取并更新state值
如果state == 0表示没有其他线程在执行同步代码,则通过CAS把state置为1 会成功,当前线程继续执行。
如果status != 0,通过CAS把state置为1 会失败,然后判断当前线程是否是获取到这个锁的线程,如果是的话执行state+1,且当前线程可以再次获取锁。 -
释放锁时,可重入锁同样先获取当前state的值,在当前线程是持有锁的线程的前提下。
如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。
你需要注意的是state变量的定义,其实AQS的实现类都是通过控制state的值来控制锁的状态的。它被volatile所修饰,能保证可见性。
private volatile int state;
扩展:如果要通过AQS的state来实现非可重入锁怎么实现呢?明确这两点就可以了:
- 获取锁时:去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
- 释放锁时:在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。
5、可重入锁的特点
可重入锁的一个优点是可一定程度避免死锁。
可重入锁能避免一定线程的等待,可想而知可重入锁性能会高于非可重入锁。你可以写程序测试一下哦!!!
沪漂程序员一枚。
坚持写博客,如果觉得还可以的话,给个小星星哦,你的支持就是我创作的动力。
个人微信公众号:“Java尖子生”,阅读更多干货。
<font color='red'>关注公众号,领取学习、面试资料。加技术讨论群。</font>