前言
面试题:synchronized是可重入锁吗?
答案:synchronized是可重入锁。ReentrantLock也是的。
1、什么是可重入锁呢?
关于什么是可重入锁,我们先来看一段维基百科的定义。
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
通俗来说:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
再换句话说:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
2、自己写代码验证下可重入和不可重入
我们启动一个线程t1,调用addOne()方法来执行加1操作。在addOne方法里面t1会获得rtl锁,然后调用get()方法,在get()方法里再次请求获取trl锁。
因为最终能打印value=1,说明t1在第二次获取锁的时候并没有阻塞。说明ReentrantLock是可重入锁。
1 | java复制代码import java.util.concurrent.locks.Lock; |
换成synchronized的加锁方式,同样能打印value的值。证明synchronized也是可重入锁。
1 | csharp复制代码public class ReentrantTest { |
3、自己如何实现一个可重入和不可重入锁呢
不可重入:
1 | java复制代码public class Lock{ |
可重入:
1 | ini复制代码public class Lock{ |
从代码实现来看,可重入锁增加了两个状态,锁的计数器和被锁的线程,实现基本上和不可重入的实现一样,如果不同的线程进来,这个锁是没有问题的,但是如果进行递归计算的时候,如果加锁,不可重入锁就会出现死锁的问题。
4、ReentrantLock如何实现可重入的
使用ReentrantLock你要知道:
ReentrantLock支持公平和非公平2种创建方式,默认创建的是非公平模式的锁。
看下它的构造方法:
1 | scss复制代码public ReentrantLock() { |
看下非公平锁,它是继承抽象类Sync的:
1 | scala复制代码static final class NonfairSync extends Sync { |
看下公平锁,它也是继承抽象类Sync的:
1 | java复制代码static final class FairSync extends Sync { |
NonfairSync、FairSync 和抽象类Sync 都是ReentrantLock的内部类。
Sync的定义,它是继承AbstractQueuedSynchronizer的,AbstractQueuedSynchronizer既是我们常说的**AQS**(后面我也会整理一篇)
1 | scala复制代码abstract static class Sync extends AbstractQueuedSynchronizer { |
好了,继承关系清楚了 ,现在我们看下ReentrantLock是如何实现可重入的
我们在addOne()和get()两个方法加锁的地方都打上断点。然后开始调式:
- addOne方法获取锁的时候走到NonfairSync的“compareAndSetState(0, 1)”,通过CAS设置state的值为1,调用成功,并设置当前锁被持有的线程为当前线程t1;
- 继续调试,get方法获取锁的时候走到NonfairSync的“compareAndSetState(0, 1)”,通过CAS设置state的值为1,调用失败(因为已经被当前线程t1锁占有),走到else里面,继续往里看;
- 走到NonfairSync的tryAcquire方法,再往里走;
- 会调用Sync抽象类里面的nonfairTryAcquire方法。源码解释我都写在下面了。
1 | java复制代码final boolean nonfairTryAcquire(int acquires) { |
到此,可重入锁加锁的过程分析完毕。解锁的过程一样,希望你能自己debug下【调用的是Sync抽象类里面的tryRelease方法】
我这里总结一下:
- 当线程尝试获取锁时,可重入锁先尝试获取并更新state值
如果state == 0表示没有其他线程在执行同步代码,则通过CAS把state置为1 会成功,当前线程继续执行。
如果status != 0,通过CAS把state置为1 会失败,然后判断当前线程是否是获取到这个锁的线程,如果是的话执行state+1,且当前线程可以再次获取锁。 - 释放锁时,可重入锁同样先获取当前state的值,在当前线程是持有锁的线程的前提下。
如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。
你需要注意的是state变量的定义,其实AQS的实现类都是通过控制state的值来控制锁的状态的。它被**volatile所修饰,能保证可见性**。
1 | arduino复制代码private volatile int state; |
扩展:如果要通过AQS的state来实现非可重入锁怎么实现呢?明确这两点就可以了:
- 获取锁时:去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
- 释放锁时:在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。
5、可重入锁的特点
可重入锁的一个优点是可**一定程度避免死锁**。
可重入锁能避免一定线程的等待,可想而知**可重入锁性能会高于非可重入锁**。你可以写程序测试一下哦!!!
推荐阅读:
本文转载自: 掘金