可重入锁
首先结合以下两个例子理解以下可重入锁的概念。
/**
* 可重入锁:
* 1、可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
* 2、是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个
* 对象),不会因为之前已经获取过还没释放而阻塞
*/publicclassReEnterLockDemo{staticObject objectLockA=newObject();publicstaticvoidm1(){newThread(()->{synchronized(objectLockA){System.out.println(Thread.currentThread().getName()+"\t"+"------外层调用");synchronized(objectLockA){System.out.println(Thread.currentThread().getName()+"\t"+"------中层调用");synchronized(objectLockA){System.out.println(Thread.currentThread().getName()+"\t"+"------内层调用");}}}},"t1").start();}publicstaticvoidmain(String[] args){m1();}}
publicclassReEnterLockDemo{publicsynchronizedvoidm1(){System.out.println("=====外层");m2();}publicsynchronizedvoidm2(){System.out.println("=====中层");m3();}publicsynchronizedvoidm3(){System.out.println("=====内层");}publicstaticvoidmain(String[] args){newReEnterLockDemo().m1();}}
相关知识的了解
3种让线程等待和唤醒的方法
- 方式1: 使用
Object
中的wait()
方法让线程等待, 使用Object中的notify()
方法唤醒线程 - 方式2: 使用JUC包中
Condition
的await()
方法让线程等待,使用signal()
方法唤醒线程 - 方式3:
LockSupport
类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类提供的等待唤醒机制的缺点
publicclassLockSupportDemo1{staticObject objectLock=newObject();publicstaticvoidmain(String[] args){newThread(()->{synchronized(objectLock){System.out.println(Thread.currentThread().getName()+"\t"+"------come in");try{
objectLock.wait();}catch(InterruptedException e){
e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");}},"A").start();newThread(()->{synchronized(objectLock){
objectLock.notify();System.out.println(Thread.currentThread().getName()+"\t"+"------通知");}},"B").start();}}
结果:
A ------come in
B ------通知
A ------被唤醒
Process finished with exit code 0
异常情况①:去掉同步代码块
publicclassLockSupportDemo1{staticObject objectLock=newObject();publicstaticvoidmain(String[] args){newThread(()->{// synchronized (objectLock){System.out.println(Thread.currentThread().getName()+"\t"+"------come in");try{
objectLock.wait();}catch(InterruptedException e){
e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");// }},"A").start();newThread(()->{// synchronized (objectLock){
objectLock.notify();System.out.println(Thread.currentThread().getName()+"\t"+"------通知");// }},"B").start();}}
结果:
A ------come in
Exception in thread "A" Exception in thread "B" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$0(LockSupportDemo1.java:16)
at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$1(LockSupportDemo1.java:26)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
异常情况②:先唤醒,再等待。
publicclassLockSupportDemo1{staticObject objectLock=newObject();publicstaticvoidmain(String[] args){newThread(()->{try{TimeUnit.SECONDS.sleep(2);}catch(InterruptedException e){
e.printStackTrace();}synchronized(objectLock){System.out.println(Thread.currentThread().getName()+"\t"+"------come in");try{
objectLock.wait();}catch(InterruptedException e){
e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");}},"A").start();newThread(()->{synchronized(objectLock){
objectLock.notify();System.out.println(Thread.currentThread().getName()+"\t"+"------通知");}},"B").start();}}
结果:
B ------通知
A ------come in
Process finished with exit code -1
死循环,A无法被唤醒了。
两种异常:
Object类提供的
wait
和notify
1、只能在
synchronized
同步代码块里使用2、只能先等待(wait),再唤醒(notify)。顺序一旦出错,那个等待线程就无法被唤醒了。
Condition类提供的等待唤醒机制的缺点
这里也有两个缺点,而且和Object类里的wait,notify几乎一样。
1、只能在
lock
同步代码块里使用,不然就报错2、只能先等待(await),再唤醒(signal)。顺序一旦错了,那个等待线程就无法被唤醒了。
但相对于wait,notify改进的一点是,可以绑定lock进行定向唤醒,或者说精确唤醒。
LockSupport(本节重点)
首先直接看示例
异常情况①:无同步代码块
publicclassLockSupportDemo3{publicstaticvoidmain(String[] args){/**
LockSupport:俗称 锁中断
LockSupport它的解决的痛点
1。LockSupport不用持有锁块,不用加锁,程序性能好,
2。不需要等待和唤醒的先后顺序,不容易导致卡死
*/Thread t1=newThread(()->{System.out.println(Thread.currentThread().getName()+"\t ----begin-时间:"+System.currentTimeMillis());LockSupport.park();//阻塞当前线程System.out.println(Thread.currentThread().getName()+"\t ----被唤醒-时间:"+System.currentTimeMillis());},"t1");
t1.start();LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName()+"\t 通知t1...");}}
结果:
t1 ----begin-时间:1603376148147
t1 ----被唤醒-时间:1603376148147
main 通知t1...
Process finished with exit code 0
没有出现任何问题。
异常情况②:先唤醒,再阻塞(等待)。
publicstaticvoidmain(String[] args){Thread t1=newThread(()->{try{TimeUnit.SECONDS.sleep(2);}catch(InterruptedException e){
e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t ----begin-时间:"+System.currentTimeMillis());LockSupport.park();//阻塞当前线程System.out.println(Thread.currentThread().getName()+"\t ----被唤醒-时间:"+System.currentTimeMillis());},"t1");
t1.start();LockSupport.unpark(t1);System.out.println(Thread.currentThread().getName()+"\t 通知t1...");}
结果:
main 通知t1...
t1 ----begin-时间:1603376257183
t1 ----被唤醒-时间:1603376257183
Process finished with exit code 0
可以看到,如果提前对线程进行唤醒。那么后面执行的LockSupport.park();
就相当于瞬间被唤醒了,不会和之前一样程序卡死。
为什么呢?
staticvoidpark()//除非许可证可用,否则禁用当前线程以进行线程调度。staticvoidunpark(Thread thread)//如果给定线程尚不可用,则为其提供许可。
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
- LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),
permit只有两个值1和零,默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
publicstaticvoidpark(){
UNSAFE.park(false,0L);}
LockSupport底层是通过UNSAFE。
-
permit默认是0,所以一开始调用
park()
方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。 -
调用
unpark(thread)
方法后,就会将thread线程的许可permit设置成1(多次调用unpark方法,不会累加,permit值还是1
),然后程序就会自动唤醒thread线程,也就是之前阻塞中的LockSupport.park()方法会立即返回。 -
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就将0变成1,调用一次park会消费permit,也就是将1变成o,同时park立即返回。如再次调用park会变成阻塞(
因为permit为零了会阻塞在这里,一直到permit变为1
),这时调用unpark会把permit置为1。
每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。 -
简单的理解就是,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时: 如果有凭证,则会直接消耗掉这个凭证然后正常退出; 如果无凭证,就必须阻塞等待凭证可用。 而unpark则相反: 它会增加一个凭证,但凭证最多只能有1个,累加无效。
因此上述问题的答案就是:
1、先执行unpark()
,将许可证由0变为1。
2、然后park()
来了发现许可证此时为0(也就是有许可证),那么他就不会阻塞,马上就往后执行。同时消耗许可证(也就是将1又变为0)。
回归正题
Java中的synchronized 和ReentrantLock 都是可重入锁。可重入锁的意义在于防止死锁。
先看下ReentrantLock的继承关系图:
ReentrantLock实现了Lock接口,对外提供Lock接口的方法。有一个同步器属性,上锁、释放锁都是通过调用同步器的相关方法实现的。构造时,同步器可以选择公平锁/非公平锁,它们都继承了抽象父类Sync,而Sync又继承了AQS。
抽象类AQS维护了等待队列,而ReentrantLock只需要定义共享资源的获取与释放的方式。
可重入功能的实现原理
ReentrantLock的可重入功能基于AQS的同步状态:state。
其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。
以下是非公平锁(默认下我们使用的都是非公平锁)中可重入的实现原理。
publicReentrantLock(){// 默认非公平锁
sync=newNonfairSync();}publicReentrantLock(boolean fair){
sync= fair?newFairSync():newNonfairSync();}
这是非公平锁的部分代码,这里只为说明可重入
的实现原理(详见注释),对于非公平锁更详细分析见后文。
// acquires的值是1finalbooleannonfairTryAcquire(int acquires){// 获取当前线程finalThread current=Thread.currentThread();// 获取state的值int c=getState();// 如果state的值等于0,表示当前没有线程持有锁// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回trueif(c==0){if(compareAndSetState(0, acquires)){setExclusiveOwnerThread(current);returntrue;}}// state的值不等于0,表示已经有其他线程持有锁// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true// 如果不是当前线程,获取锁失败,返回falseelseif(current==getExclusiveOwnerThread()){int nextc= c+ acquires;if(nextc<0)// overflowthrownewError("Maximum lock count exceeded");setState(nextc);returntrue;}returnfalse;}
ReentrantLock中非公平和公平的实现原理
因为ReentrantLock同时支持公平锁和非公平锁,上文也提到,ReentrantLock默认无参构造函数使用的是非公平锁,有参构造函数可指定使用公平锁。
对于公平锁:是指在获取锁之前会检查队列中有没有线程在等待,如果有的话就不会去获取锁,而是会从尾结点加入队列。
对于非公平锁:就是在获取锁之前不会去检查队列中有没有线程在等待,而是直接去获取锁,这里其实是一种插队
的表现。如果锁没有线程占用,则队列中被唤醒的线程和新来的线程会同时竞争锁。此时,队列中被唤醒的线程并不一定能优先获得锁,当队列中被唤醒的线程被新来的线程抢占了资源,这种插队也就表现出了非公平的特性。
非公平锁的获取
注意和后面公平锁的对比
staticfinalclassNonfairSyncextendsSync{finalvoidlock(){// 和公平锁相比,这里会直接先进行一次CAS(尝试插队),成功就返回了。这是第一处不一样【对比请看下方公平锁的lock】if(compareAndSetState(0,1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// AbstractQueuedSynchronizer类的acquire(int arg)方法publicfinalvoidacquire(int arg){if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}protectedfinalbooleantryAcquire(int acquires){returnnonfairTryAcquire(acquires);}}// acquires的值是1finalbooleannonfairTryAcquire(int acquires){// 获取当前线程finalThread current=Thread.currentThread();// 获取state的值int c=getState();// 如果state的值等于0,表示当前没有线程持有锁// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回trueif(c==0){// 这里没有对队列进行判断,直接CAS抢,这是第二点不一样【对比请看下方公平锁的lock】if(compareAndSetState(0, acquires)){//获取成功就设置线程变量setExclusiveOwnerThread(current);returntrue;}}// state的值不等于0,表示已经有其他线程持有锁// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true// 如果不是当前线程,获取锁失败,返回falseelseif(current==getExclusiveOwnerThread()){int nextc= c+ acquires;// 如果小