多线程之死锁

谁践踏了优雅 2024-03-30 10:44 231阅读 0赞

目录:

1.什么是死锁?

2.可重入与不可重入

3.发生死锁的三个典型情况

4.发生死锁的四个必要条件

5.如何破除死锁?



1.什么是死锁?

谈到死锁,程序猿们都心存忌惮,因为程序一旦出现死锁,就会导致线程无法继续执行后面的工作了,那么这个程序就会出现非常大的bug。而且死锁一般来说,不易发现,在开发阶段如果写出了死锁代码,那么很可能测试是测试不出来的,所以出现死锁问题还是非常不爽的。下面将详细谈谈关于死锁。

2.可重入与不可重入

一个线程针对同一个对象连续加锁两次。如果没有出现问题就是可重入的,如果出现问题了那么就是不可重入的。

c0337d24995a4ba8b8939b5601e19ecd.png

此时锁对象是this,只要有线程调用add方法,那么进入add方法的时候就会先加锁,进去之后又遇到了代码块,将再次尝试加锁。站在this(锁对象)的角度,它认为自己已经被线程占用了,这里的第二次加锁是否要阻塞等待呢?如果允许第二次加锁,那么这个锁就是可重入的;如果不允许第二次加锁,第二次加锁会发生阻塞等待,那么就是不可重入的,这个时候就发生僵持了,就发生死锁了。在java中,把synchronized设定成可重入的了。

3.发生死锁的三个典型情况

1.一个线程针一把锁连续加锁两次。如果锁是不可重入锁,就会死锁。

2.两个线程两把锁,t1先对锁A加锁,t2先对锁B加锁,然后各自再尝试获取对方的锁,就会死锁。代码示例:

  1. public class ThreadDemo10 {
  2. public static void main(String[] args) {
  3. Object A = new Object();
  4. Object B = new Object();
  5. Thread t1 = new Thread(()->{
  6. synchronized (A){
  7. try {
  8. Thread.sleep(1000);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. synchronized (B){
  13. System.out.println("A去拿B的锁了");
  14. }
  15. }
  16. });
  17. Thread t2 = new Thread(()->{
  18. synchronized (B){
  19. try {
  20. Thread.sleep(1000);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. synchronized (A){
  25. System.out.println("B去拿A的锁了");
  26. }
  27. }
  28. });
  29. }
  30. }

运行结果如下:(出现死锁了)

6986747501b749369df0715716126a6e.png

用jconsole查看线程情况:

6f2434baf82f4d81a75e06764f05f3c9.png

4c29b004c1644dadb97e27e262cf76e0.png 这里很明显可以看到这两把锁互相僵持住了,出现了死锁问题。针对这样的死锁问题,也是需要借助像jconsole 这样的工具来进行定位的,看线程的状态和调用栈,就可以分析出代码是在哪里死锁了。

3.多个线程多把锁

因为由于操作系统的随机调度,多个线程对多把锁可能会随机加锁,那么就会出现第二点那样的情况,相互僵持,出现死锁。如何解决这个问题呢?可以把锁进行编号,然后对线程进行约定,默认从编号小的开始加锁,这样就可以死锁问题。然后我们将第二点的实例代码改造一下,默认都先对A加锁,再对B进行加锁,这样就不会出现死锁了。代码示例:

  1. Thread t1 = new Thread(()->{
  2. synchronized (A){
  3. try {
  4. Thread.sleep(1000);
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. synchronized (B){
  9. System.out.println("t1把A和B都拿到了");
  10. }
  11. }
  12. });
  13. Thread t2 = new Thread(()->{
  14. synchronized (A){
  15. try {
  16. Thread.sleep(1000);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. synchronized (B){
  21. System.out.println("t2把A和B都拿到了");
  22. }
  23. }
  24. });

运行结果如下:

dcb30bd895f941049ee9f0971df78228.png

4.发生死锁的四个必要条件

1.互斥使用,线程1拿到了锁,线程2就得等着。(锁的基本特性)

2.不可抢占,线程1拿到锁之后,必须是线程1主动释放,不能说是线程2就把锁给强行获取到。

3.请求和保持,线程1拿到锁A之后,再尝试获取锁B,A这把锁还是保持的。(不会因为获取锁B就把A给释放了)

4.循环等待,线程1尝试获取到锁A和锁B,线程2尝试获取到锁B和锁A。线程1在获取B的时候等待线程2释放B;同时线程2在获取A的时候等待线程1释放A

5.如何破除死锁?

在上面也已经谈到了,破除死锁的办法就是给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁。任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待就自然破除了。

发表评论

表情:
评论列表 (有 0 条评论,231人围观)

还没有评论,来说两句吧...

相关阅读

    相关 线

    死锁: 如果一个资源被一个线程占用,而且无法释放线程资源,导致其他线程无法访问这个字段,一直处于等待状态,这样就会形成 线程死锁。 例子: package com.

    相关 线

    / 死锁:二个线程同时锁住一个变量时。 锁住一个变量之后,尽快操作完成解锁,解锁之前不要再锁住其它变量,否则会互锁(死锁)。 /

    相关 线

    峨眉山月半轮秋,影入平羌江水流 Java线程的死锁一直都是经典的多线程问题;因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都不能继续执行; 示例代码:

    相关 线

    同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。