用幽默浅显的言语来说死锁
半生:我已经拿到了机考的第一名,就差笔试第一名了
小一:我已经拿到了笔试的第一名,就差机考第一名了
面试官:我很看好你俩,继续"干", 同时拿到2个的第一名才能拿到offer,进入我XX大厂
半生:小一,你的笔试第一名,让我可好?
小一:做梦,我还要你的机考第一名呢!
半生:就你那水平,zz
小一:WTF,来干一架
半生:来呀,who怕who
于是:死锁产生了,狭路相逢勇者胜,电脑死机了。
什么是死锁?死锁的产生条件是什么?
1.死锁是指两个或多个线程互相等待对方释放所持有的资源,从而导致进程无法继续执行的一种情况。具体来说,死锁发生时线程会进入一个永久等待的状态,无法继续执行并最终导致程序无响应或崩溃。
2.产生死锁的条件,通常被称为死锁的四个必要条件,包括:
- 互斥条件(Mutual Exclusion):至少有一个资源被多个线程独占,也就是说一个资源同时只能被一个线程占用。
- 请求与保持条件(Hold and Wait):线程已经持有了至少一个资源,并且在等待获取其他线程占有的资源。
- 不可剥夺条件(No Preemption):已经获得的资源不能被其他线程抢占,只能由占有它的线程显示地释放。
- 循环等待条件(Circular Wait):多个线程形成一个循环等待的等待链,即每个线程都在等待下一个线程所持有的资源。
只有当上述四个条件同时满足时,死锁才可能发生。
下面上一段代码(有问题的代码,考考你们的眼力跟基本功)
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1:锁住offer1");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 1:锁住offer2");
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2:锁住offer2");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread 2:锁住offer1");
}
});
thread1.start();
thread2.start();
}
为什么这2个线程都锁不住这个offer?
肥水不流外人田,既然你们都锁不住这个offer,就让我来~
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("半生:锁住了机考第一名");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("半生:笔试第一名");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("小一:锁住了笔试第一名");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("小一:锁住机考第一名");
}
}
});
thread1.start();
thread2.start();
}
又是你俩,干啥呢,死锁了呀~offer不止一个呀,就不能友好相处,快乐的玩耍同时都拿到offer么
让我来looklook一下字节码指令是怎么去执行的
1. 先使用javac -encoding UTF-8 X.java, 来生成class文件
2. javap -verbose X.class 反编译
3.从反编译的指令来看,这里应该是操作系统或者jvm虚拟机检查到了这是个死锁,强制中断了,在使用synchronized作为锁的时候,我们知道是有monitorenter monitorexit 这一对指令的,但是这里就没有看到
4. 下载了个idea的插件jclasslib
来查看,看字节码指令是否一致
5.可以看到这里是有这一对锁指令的,这里稍微解释下上写字节码指令的含义
0 aload_0:加载索引为0的引用到操作数栈,通常用于加载实例方法的隐式参数,即this。
1 dup:复制栈顶的元素,并将复制后的值重新压入栈顶。
2 astore_2:将栈顶的引用类型数值存储到局部变量表的索引为2的位置。
3 monitorenter:进入同步块前获取锁。
4 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>:获取静态字段System.out的值,即标准输出流PrintStream对象。
7 ldc #12 <小一:锁住了笔试第一名>:将常量池中索引为12的String类型常量加载到操作数栈。
9 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>:执行PrintStream对象的println方法,其中参数为栈顶的String类型常量。
12 ldc2_w #13 <1000>:将常量池中索引为13的long类型常量加载到操作数栈。
15 invokestatic #15 <java/lang/Thread.sleep : (J)V>:执行Thread类的静态方法sleep,其中参数为栈顶的long类型常量。
18 goto 33 (+15):无条件跳转到字节码指令33,即跳过下方的指令。
21 astore_3:将栈顶的引用类型数值存储到局部变量表的索引为3的位置。
22 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>:获取静态字段System.out的值。
25 ldc #17 <小一:被中断,释放笔试资源>:将常量池中索引为17的String类型常量加载到操作数栈。
27 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>:执行PrintStream对象的println方法,其中参数为栈顶的String类型常量。
30 aload_2:加载局部变量表中索引为2的引用类型数值到操作数栈。
31 monitorexit:退出同步块,释放锁。
32 return:返回void类型的值,并结束当前方法。
33 aload_1:加载局部变量表中索引为1的引用类型数值到操作数栈。
34 dup:复制栈顶的元素,并将复制后的值重新压入栈顶。
35 astore_3:将栈顶的引用类型数值存储到局部变量表的索引为3的位置。
这里的指令
第9 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>:执行PrintStream对象的println方法,其中参数为栈顶的String类型常量。执行了catch中的打印日志,说明被执行中断了,后面goto 33 跳到33行的指令
6.此外我另外写了同步方法,来看看
从这里看出,它是有加锁的,第20行多了一个monitorexit,这就是防止异常强制释放锁,也就是synchronized能自动释放锁的保障
于是:解决死锁的方式来了~
退一步想阔天空,你好我好大家好
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("半生:获得了机考第一名");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("半生:获得了笔试第一名");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource1) {
System.out.println("小一:获得了笔试第一名");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("小一:获得了机考第一名");
}
}
});
thread1.start();
thread2.start();
}
只要稍微改下获取资源的顺序,半生跟小一就分别都获取了机考,笔试第一名,都收到了XX大厂offer
打破死锁的方式有多种,只要四个死锁的必要条件去其一就可以了
常用的有以下几种常见的方式可以用来解决死锁问题:
避免循环等待:通过对资源加锁的顺序进行规定,以避免线程之间互相等待对方所持有的资源。可以通过排序或编号等方式来约定资源的获取顺序,从而避免循环等待。
破坏请求与保持条件:允许线程在请求资源时一次性获取所有需要的资源,或者在获取某个资源时释放已经占有的资源。这样可以避免一个线程持有一个资源而等待另一个资源被释放的情况。
使用资源剥夺:当一个线程请求资源时,如果资源已经被其他线程占有,则可以暂时剥夺其他线程对该资源的锁定,以满足当前线程的需求。被剥夺的线程可以等待一段时间后再重新申请资源。
使用超时机制:在获取锁资源时设置一个超时时间,在规定时间内无法获取到资源则放弃获取,释放已占有的资源,然后重新尝试。
死锁检测和恢复:通过检测系统中的死锁情况,对存在死锁的线程进行恢复或终止。常见的死锁检测算法包括资源分配图算法和银行家算法。
需要注意的是,不同的解决方式适用于不同的场景和问题,选择合适的方式需要根据具体情况进行评估。另外,预防死锁问题是更好的做法。在设计和实现时,尽量避免存在可能导致死锁的条件,从根本上杜绝死锁问题的发生。