大家先猜一下下面这个代码是否可以成功运行?
java">Thread t = new Thread(() - >{
synchronized(locker){
synchronized(locker){
//..随便写点啥都行
System.out.println("hello");}}
});
t.start();
从直观上感觉,这个加锁应该是不能成功呀!! 此时locker对象处于已经加锁的状态~~
这个时候,如果你要是再尝试对 locker 加锁 不就会出现“阻塞” 情况吗~~
为啥最终没出现阻塞呢??
最关键问题,在于说,这两次加锁,其实是在同一个线程上进行的!!
当前由于是同一个线程,此时锁对象,就知道了第二次加锁的线程,就是持有锁的线程,第二次操作,就可以直接放行通过不会出现阻塞~~ 这个特性,称为“可重入”
上述代码中真正加锁,同时把计数器 + 1(初始情况是0,+1之后变成1了,说明当前这个对象被该线程加锁异常)同时记录线程是谁~~
对于可重锁来说,内部会持有两个信息
1.当前这个锁是被哪个线程持有的~~
2.加锁次数的计数器
即使上述synchronized 嵌套个10层8层,也不会使解锁操作混乱,始终能够保持在正确的时机解锁
“死锁”
死锁是多线程代码中的一类经典问题~~
加锁是能够解决线程安全问题,但如果加锁方式不当,就可能产生“死锁”!!
1.死锁的三种典型场景~~
1.一个线程,一把锁。
刚才说情况,如果锁是不可重入锁,并且一个线程对这把锁加两次就会出现死锁~~
2.两个线程,两把锁。
线程1获取到锁A,线程2获取到锁B,接下来,1尝试获取锁B,2尝试获取锁A,就同样出现死锁了!!
一旦出现死锁,线程就“卡住了”无法继续工作,死锁,属于程序中最严重的一类bug!!
java">import static java.lang.Thread.sleep;public class ThreadDemo17 {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(()->{synchronized (A){//sleep 一下,是给 t2 时间,让 t2也能拿到 Btry {sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} synchronized (B){System.out.println("t1 拿到了两把锁");}}});Thread t2 = new Thread(()->{synchronized (B){try {sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (A){System.out.println("t2 拿到了两把锁");}}});t1.start();t2.start();}
}
他们彼此在等待对方解锁,但结果一直等不到。(钥匙锁车里,车钥匙锁屋里)
3.N个线程M把锁
哲学家就餐问题(学校的操作系统课,也有这个东西)
如果某个哲学家,在吃面条的过程中,旁边有两个老哥,就需要阻塞等待~~
(五双筷子,相当于五把锁)
五个老哥,就是应该线程。
当前线程拿到锁的时候,就会一直持有,除非他吃完,主动放下筷子,其他伙计不能硬抢!!
虽然筷子数量不多不充裕,但也还好~~
每个哲学家,除了吃面之外,还要做一件事,“思考人生”,是会放下筷子的~~
由于每个哲学家,啥时候吃面条,啥时候思考人生,这个事情不确定~~(随机调度)
绝大部分下,上述模型都是可以正常工作的~~
但是有一些极端的特殊情况,是不行的!!
解决死锁问题,方法有很多。
先解释一下,产生死锁的四个必要条件
2.产生死锁的四个必要条件
1.互斥使用。获取锁的过程是互斥的, 一个线程拿到了这把锁,另一个线程也想获取就需要阻塞等待。锁基本的特性,不太好破坏~~
2.不可抢占。一个线程拿到了锁之后,只能主动解锁,不能让别的线程强行把锁带走~~锁基本的特性,不太好破坏~~
3.请求保持。一个线程拿到了锁A之后,在持有锁A前提下,尝试获取锁B。代码结构不一定,看实际需求~~~
4.循环等待/环路等待 代码结构的,最容易破坏
只要指定一定的规则,就可以有效避免循环等待
指定加锁顺序
针对五把锁,都进行编号,约定每个线程获取锁的时候,一定都先获取编号小的锁,后就获取编号大的锁!!!
上述这样调一下就好了,大前提,是“随机调度”想办法,让某个线程先加锁,未被“随机调度”根本原则可行性是不高的~~~
而约定加锁顺序,在写代码的层面上是非常容易做到的~~
3.解决死锁方案
1.引入额外的筷子(虽然方案复杂性不高,但普适性不强)
2.去掉一个线程(虽然方案复杂性不高,但普适性不强)
3.引入计数器,限制最多同时多少人吃面(虽然方案复杂性不高,但普适性不强)
4.引入加锁顺序的规则 (普适性高,方案容易落地)
5.学校操作系统课“银行家算法”(理论可行,实践不推荐)