讲讲JAVA中的死锁问题

来自:程序员孤独烟(微信号:guduyan_zrj),作者:孤独烟

引言

今天在网上看到了一个图片,嗯,似乎给自己的未来找到了方向:

大家要努力!
嗯,开始我们的正题。今天我们来讲讲java中的死锁问题,大致分为下面三个小点
  • 如何检测死锁

  • 如何预防死锁

  • 隐蔽的死锁

正文

如何检测死锁

首先,我们先明白在什么情况下会怀疑是死锁?
简单,就是程序没有响应的时候。其实排查步骤和《谈谈线上CPU100%排查套路》是类似的。但是有一个区别,在死锁的情况下,cpu不会跑满。
也就是说,当你发现程序没有响应,cpu占用率又不是特别高的时候,第一反应就是应该是死锁
那么,死锁发生的情形也很简单,如下图所示,

我们准备一段程序,模拟一下


排查步骤也很简单,执行下面的jps命令看看当前执行任务的进程号
D:\Java>jps
22320 Launcher
27488
25612 Jps
6700 TestDeadLock

然后用jstack命令打印出6700这个进程中,线程的信息

D:\Java>jstack 6700

输出结果中,有这么一段,就能看到死锁线程信息

多嘴一句,在输出结果中你能看到线程的各种状态,如下所示

Thread的源码中有一个State枚举类定义了这些线程状态,转换关系如下(图片出自网络)

初始状态
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
就绪状态
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。
运行中状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
阻塞状态
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
等待
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
超时等待
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
终止状态
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

如何预防死锁

尽量保证加锁顺序是一样的
例如有A,B,C三把锁。
Thread 1的加锁顺序为A、B、C这样的。
Thread 2的加锁顺序为A、C
这样就不会死锁。
如果Thread2的加锁顺序为B、A或者C、A这样顺序就不一致了,就会出现死锁问题。
尽量用超时放弃机制
Lock接口提供了tryLock(long time, TimeUnit unit)方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。可以避免死锁问题

隐蔽的死锁

因为我在生产上碰到过几次死锁,线程日志里是看不到死锁信息的。一般这种情况下都是直接看线程栈的内容,自己分析。
例如下面这个并发加载类导致的死锁

代码很简单,准备两个线程并行加载,在A类中加载B类。而在B类中加载A类。就会导致死锁,此时按上述步骤是看不到死锁信息的。你只能看到下面这样的信息

线程都是处在Runable状态的,看不到死锁信息。
原因也很简单:
static代码里头的内容会被编译放到clinit方法里。JVM执行clinit时会给clinit加上锁,防止多线程并发执行。两个线程在初始化的时候,先各自加上clinit锁。然后在通过Class.forName去加载对方,因此都想获取对方要执行clinit的锁,因此死锁就此发生。

总结

本文讲了java中死锁相关知识,希望大家有所收获!

推荐↓↓↓
Java编程
上一篇:编码不规范,同事真的会两行泪? 下一篇:微服务架构之「 API网关 」