概述
简而言之,一个线程,连续针对一把锁,连续加锁两次或以上,就有可能出现两种情况,一种是产生死锁,这样的锁叫做“不可重入锁”,另一种是不会产生死锁,这个锁叫做“可重入锁”;
深入理解
什么是死锁?
一个有趣的比方,假如,你是八路军~ 的一个秘密的电报员,在入敌深处,建立了一个隐蔽的房间专门给组织发送机密电报;这次你又发现了重要敌情,准备给组织发送情报,那么这么重要的事肯定不能敞开天窗大门来干,于是呢,你关上大门,为了防止闲杂人等误入,你就给门上了锁...发送完情报,忽然发现门外有很多日军在闲逛!!!为了避嫌,你选择了从这个只有你知道的地下道出去~ 过了一阵一段时间,你的同事,也来这个地方发送情报,发现门锁了!他以为你在里面发送重要情报,就没有打扰你,于是就一直在门口等下去了......
让我们来看看实际的代码中是如何出现的吧!(如下代码)
这就产生了死锁!也就是说,当一个线程对一把锁,连续加锁两次;
分析:第一次加锁,可以加锁成功,第二次加锁,就会失败,因为锁以及被占用,于是会一直在这里阻塞等待,等到第一把锁解锁,第二把锁才能加锁成功,但想要第一把锁解锁,需要执行完synchronized代码块,才可以加下一把锁,然而第二把锁一直在阻塞等待,所以第一把锁既不能解锁,第二把锁也不能加锁,就卡在这里了;
有的人可能就会说:俺作为一个优秀的程序员,怎么会写出这样的代码呢?
优秀名句——我写的代码怎么会有BUG呢?
但实际上却是防不胜防——例如多次嵌套,直观上看出不来~
你看!这BUG,他不就来了吗!
但实际上,以上程序不会死锁,这就关系到synchronized关键字的底层设计了...
如何解决以上问题呢?(可重入锁的底层逻辑是什么?)
实际上它的底层很简单,只要让这个锁记住,是哪个线程持有这把锁就OK~
什么意思呢?两次连续加锁:假设 t1 线程通过this来加锁,那么这个this里面就记录了是t线程持有的他,第二次加锁的时候,再一看锁,如果还是 t1 线程,就直接通过,不会再有阻塞等待!
这又是怎么做到的呢?实际上是引入了一个计数器,每次加锁,计数器就++,解锁的时候计数器就--,若计数器为零,这时候才可以进行加锁(真加锁),同样,若计数器为零,此时才真正解了锁(真解锁);
计数器还未归0,程序就抛出异常,会不会死锁?
分析:若程序抛出异常,并且没有catch捕捉,程序就会脱离之前的代码块,一旦脱离这层加锁的代码块,计数器就会--,脱离多层代码块,计数器减到0,也就解锁了;
总结:加锁时若出现异常,是不会死锁的,也是一个使得synchronized优秀到将他设计成关键字的原因了,若是C++/Python加锁解锁,都是通过对象来实现的,这时就有可能由于出现异常引起代码未执行完,解锁代码未执行引起死锁;
面试题:可重入锁的实现要点
- 让锁持有线程对象信息,记录是谁加的锁;
- 维护一个计数器,用来判定是什么时候真加锁,什么时候真解锁,什么时候是直接放行。