关于java:并发王者课黄金2行稳致远如何让你的线程免于死锁

66次阅读

共计 3101 个字符,预计需要花费 8 分钟才能阅读完成。

欢送来到《并发王者课》,本文是该系列文章中的 第 12 篇

在上篇文章中,咱们介绍了死锁的概念及其起因,本文将为你介绍的是几种常见的死锁预防策略

简略来说,预防死锁次要有三种策略:

  • 程序化加锁;
  • 给锁一个超时期限;
  • 检测死锁。

一、程序化加锁

通常,死锁的产生是因为多个线程 无序申请 资源造成的。资源是无限的,不可能同时满足所有线程的申请。然而,如果能依照肯定的程序别离满足各个线程的申请,那么死锁也就不再存在,也就是所谓的程序化加锁(Lock Ordering)

举个艰深点的例子,烦人的路口堵车你肯定遇到过。路口之所以堵车,是因为车太多了,大家都争相往本人的方向去,互不相让,不堵才怪。这时候,就须要交警在两头进行协调指挥,疏解拥挤。交警之所以可疏解拥挤,其根本原因在于,交警让本来处于 无序竞争 的车流变成了 颠三倒四 的队列。

车还是那么多的车,路口还是那个路口,可是路线通顺了,这和线程的锁竞争是相似的情理。

在上篇文章的死锁代码中,线程 1 和线程 2 别离先占有了 A 和 B,导致了死锁。依照方才的思路,咱们把程序调整下,线程 1 和线程 2 都先占有 A,而后再同时抢夺 B,那么死锁就不会产生。

定义哪吒线程 1,先抢占 A 再抢夺B

private static class NeZha implements Runnable {public void run() {synchronized(lockA) {System.out.println("哪吒: 持有 A!");

      try {Thread.sleep(10);
      } catch (InterruptedException ignored) {}
      System.out.println("哪吒: 期待 B...");

      synchronized(lockB) {System.out.println("哪吒: 曾经同时持有 A 和 B...");
      }
    }
  }
}

定义兰陵王线程 2,也是先抢占 A 再抢占 B 这与此前就不同了

private static class LanLingWang implements Runnable {public void run() {synchronized(lockA) {System.out.println("兰陵王: 持有 A!");

      try {Thread.sleep(10);
      } catch (InterruptedException ignored) {}
      System.out.println("兰陵王: 期待 B...");

      synchronized(lockB) {System.out.println("兰陵王: 曾经同时持有 A 和 B...");
      }
    }
  }
}

启动两个线程:

public class DeadLockDemo {public static final Object lockA = new Object();
    public static final Object lockB = new Object();

    public static void main(String args[]) {Thread thread1 = new Thread(new NeZha());
        Thread thread2 = new Thread(new LanLingWang());
        thread1.start();
        thread2.start();}
}

两个线程的输入后果如下:

哪吒: 持有 A!
哪吒: 期待 B...
哪吒: 曾经同时持有 A 和 B...
兰陵王: 持有 A!
兰陵王: 期待 B...
兰陵王: 曾经同时持有 A 和 B...

从运行的后果中能够看到,两个线程都先后取得了本人所须要的资源,而没有导致死锁

调整加锁程序是一种简略但无效的死锁预防策略。然而,这一策略并不是万能的,它仅实用于你在编码时曾经通晓加锁的程序。

二、给锁一个超时期限

在上篇文章中,咱们说过死锁的产生有一些必要的条件,其中一个是 有限期待 。设定锁超时工夫正是为了突破这一条件, 让有限期待变成无限期待

依然以后面的代码为例,哪吒和兰陵王两个线程在抢夺资源时,对方都互不相让导致了有限期待的僵局。而此时,如果其中任何一方给期待设定一个期限,那么工夫一到,僵局将不攻自破,而线程仍能够再稍等片刻后持续尝试。

须要留神的是,synchronized代码块不能够指定锁超时。所以,如果须要锁超时,你须要应用自定义锁,或者应用 JDK 提供的并发工具类。相干工具类的用法,会在后续文章中介绍,本文暂不开展形容。

另外,所谓给锁加一个超时的期限,其实有两层含意。一是在申请锁时须要设定超时工夫,二是在获取锁之后对锁的持有也要有个超时工夫,总不能到手就不放,那是耍流氓

三、死锁检测

作为死锁预防的第三种策略,你能够认为 死锁检测(Deadlock Detection)是一项较重的被动技能,当咱们无奈程序化加锁,也无奈设置锁的超时工夫,那么就须要进行死锁检测。

死锁检测的外围原理在于对线程和资源进行数据化打标和跟踪。

在线程获取锁时,会将锁和线程的对应关系通过 Graph 或者 Map 等数据结构记录下来。这样一来,线程在获取锁被回绝时,能够通过 遍历 曾经记录的数据分析是否存在死锁。

当线程发现死锁的状况后,能够采取开释锁,稍等片刻后再次尝试。

附、如何可视化查看线程死锁等状态

在你感觉线程可能被阻塞或死锁时,能够通过 jstack 命令查看。如果存在死锁,输入的后果中会有明确的死锁提醒,如上面所示:

$ jstack -F 8321
Attaching to process ID 8321, please wait...
Debugger attached successfully.
Client compiler detected.
JVM version is 1.6.0-rc-b100
Deadlock Detection:

Found one Java-level deadlock:
=============================

"Thread2":
  waiting to lock Monitor@0x000af398 (Object@0xf819aa10, a java/lang/String),
  which is held by "Thread1"
"Thread1":
  waiting to lock Monitor@0x000af400 (Object@0xf819aa48, a java/lang/String),
  which is held by "Thread2"

Found a total of 1 deadlock.

除了 jstack 之外,JProfiler 也是一款十分弱小的线程与堆栈剖析工具,并能够和 IDEA 等 IDE 完满联合。

借助于 JProfiler,咱们能够十分直观地看到上述示例代码中的死锁,也能够在 Thread Monitor 中看到两个线程的状态为blocked.


须要留神的是,JProfiler 是一款付费软件,它提供了十天的收费试用工夫。如果没有惯例的应用需要,而是仅用于学习的话,十天也是够用的。当然,你也能够思考应用 jConsole、jVisualvm 等。

小结

以上就是对于死锁预防策略的全部内容。在本文中,咱们介绍了三种死锁预发策略。三种策略各有利弊,就理论工作中的利用而言,第二种给锁设定超时期限是更为罕用的一种做法,而第一种和第三种具备肯定的逻辑难度和技术难度,更侧重于了解而非理论利用。

注释到此结束,祝贺你又上了一颗星✨

夫子的试炼

  • 通过 jstack 命令查看死锁并解决。

延长浏览与参考资料

  • Deadlock Prevention
  • 《并发王者课》纲要与更新进度总览

对于作者

关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶然也聊聊生存和现实。不贩卖焦虑,不做题目党。

如果本文对你有帮忙,欢送 点赞 关注 监督 ,咱们一起 从青铜到王者

正文完
 0