@TOC
一:什么是死锁???
public class Demo1 {
public static void main(String[] args) {
Object locker=new Object();
Thread thread=new Thread(()->{
synchronized(locker){
synchronized (locker){
System.out.println("hello thread");
}
}
});
thread.start();
}
}
上述代码:thread一共加了两次锁,第一次加锁,肯定是能够成功的,但当第二次加锁的时候,此时,第一次加锁后,还未进行解锁操作,那么第二次加锁操作就不能获得锁对象,就会加锁失败,那么该线程就会进入阻塞等待,等待到第一次加锁 操作完了,释放锁操作,但第一次的操作要释放锁,那么必须执行完第二次加锁,解锁操作(代码顺序执行).
这样就会造成第二次加锁操作阻塞等待,等第一次操作释放锁,由于代码顺序执行,第一次要想释放锁,那必须执行完第二次的synchronized操作,那么就非常矛盾,这种情况,就叫做死锁.
二:可重入锁
当我们运行程序的时候,发现代码正常执行了,并没有进入死锁状态.
这是为什么???
是因为synchronized,对上述情况做出处理(JVM).
每个锁对象里,会记录当前是哪个线程持有了这个锁,当针对这个锁对象进行加锁操作的时候,就会先判定一下,当前尝试加锁的线程,是否是持有锁的线程,如果是,直接进行加锁操作,如果不是,那就阻塞等待.这种锁也就作可重入锁
synchronized(locker){
synchronized (locker){
System.out.println("hello thread");
}//1
}//2
上面代码,当加了2层锁的时候,代码执行到哪里是要真正的进行解锁操作呢?
肯定是在2这里解锁(最外层的}),否则,如果在1解锁,那么1和2中间有代码的话,是没有被保护起来的.
同理:如果加了N层锁,是如何判定遇到的**}是最外层的呢?
其实,JVM会给锁对象维护一个计数器(int n),每次遇到{** 就加1,(只有第一次才是真正的加锁),每次遇到**}**,n就-1,当n=0的时候,就是真正的解锁.
三:死锁的典型场景:
1:场景一:不可重入锁
锁是不可重入锁,并且一下线程针对一个锁对象,连续加锁多次,
引入可重入锁,问题就解决了.
2:场景二:两个线程,两把锁
有线程1和线程2,锁A和锁B;现在,线程1拿到了锁A,线程2拿到了锁B,然后线程1想要获取锁B,就需要阻塞等待,等待线程2 释放锁B,同理线程2想要获取到锁A,就需要等待线程1释放锁A,两个线程都进入阻塞等待,那么就会引起死锁问题.
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
synchronized(locker1){
try {
Thread.sleep(1000);//sleep()是为了让t2线程拿到另一把锁
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2){
System.out.println("hello t1");
}
}
});
Thread t2=new Thread(()->{
synchronized(locker2){
synchronized (locker1){
System.out.println("hello t2");
}
}
});
t1.start();
t2.start();
}
}
此时:t1 尝试对locker2加锁,就会阻塞等待,等待t2释放locker2;t2尝试对locker1加锁,也会阻塞等待,等待t1释放locker1.
代码执行结果:进程没有退出,也没有打印任何的内容,死锁了.
通过jconslole可以看到这里线程的状态:
3:场景三:N个线程,M把锁
哲学家就餐问题:
每个滑稽都坐在两个筷子之间.每个滑稽要做两件事:
1:思考人生(放下筷子),
2:吃面条(拿起左右两根筷子)
每个哲学家什么时候吃面条,什么时候思考人生,都是不确定的(线程抢占式执行)
那么就会出现极端情况,同一时刻,所以的滑稽都拿起左边的筷子,此时,所以的滑稽都无法拿起右边的筷子,并且每个滑稽都是固执的人(每个哲学家只有吃不到面条,绝不会放下手中的筷子),那么就会引起死锁问题
四:如何解决死锁问题???
死锁,是非常严重的问题,就会使线程阻塞等待,使线程卡住了,没法正常工作了,更可怕的是,死锁这种bug,往往都是概率性出现.
那么就必须解决死锁问题,同时线程安全问题也必须解决.
4.1:死锁的4个必要条件
1:锁具有互斥特性(锁的基本特点,一个线程拿到锁之后,其他线程要想拿到这个锁,就必须阻塞等待).
2:锁不可抢占(不可被剥夺):一个线程拿到锁之后,除非它自己主动释放锁,否则其他线程抢不走(锁的基本特点).
3:请求和保持(嵌套锁):一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取别的锁.
4:循环等待:多个线程获取多个锁的过程中,出现了循环等待.A线程等待B,B线程等待A.
若要构成死锁,这4个条件缺一不可,那么如果要解决死锁问题,就要从这四个条件下手.而条件1和条件2,是锁的基本特性,无法修改.
4.2:从条件3解决死锁问题:
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
synchronized(locker1){
try {
Thread.sleep(1000);//sleep()是为了让t2线程拿到另一把锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (locker2){
System.out.println("hello t1");
}
});
Thread t2=new Thread(()->{
synchronized(locker2){
}
synchronized (locker1){
System.out.println("hello t2");
}
});
t1.start();
t2.start();
}
}
但有的情况下,嵌套锁的情况必须存在,那么只好用另一种方法了.
4.3:从条件4解决死锁问题:
public class Demo3 {
public static void main(String[] args) {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
synchronized (locker1){
synchronized (locker2){
System.out.println(" t1获得了两把锁");
}
}
});
Thread t2=new Thread(()->{
synchronized (locker1){
synchronized (locker2){
System.out.println(" t2获得了两把锁");
}
}
});
t1.start();
t2.start();
}
}