死锁应该可以说是并发编程中比较常见的一种情况, 可以说如果程序产生了死锁那将会对程序带来致命的影响; 所以排查定位, 修复死锁至关重要;
我们都知道死锁是由于多个对象或多个线程之间相互需要 对方锁持有的锁而又没有释放对方所持有的锁, 导致双方都永久处于阻塞状态 ;
Java 中死锁的定位与修复
如上图所示, 线程 1 持有对象 1 的锁, 线程 2 持有对象 2 的锁, 持此线程 1 又想去获取对象 2 对象锁, 线程 2 想获取对象 1 对象锁, 此时由于双方都没有获取到想要的锁, 任务没完成所以也没释放锁, 导致一直僵持呢, 于是阻塞, 产生死锁;
死锁检测
需要检测死锁肯定要先有死锁出现, 下面的 demo 模拟了一个死锁的产生;
- public class DeadlockDemo extends Thread {
- private BaseObj first;
- private BaseObj second;
- public DeadlockDemo(String name, BaseObj first, BaseObj second) {
- super(name);
- this.first = first;
- this.second = second;
- }
- public void reentrantLock() throws InterruptedException {
- first.lock();
- System.out.println(String.format("%s 持有:%s 对象锁, 等待获取:%s 对象锁", this.getName(), first, second));
- second.lock();
- first.unlock();
- second.unlock();
- }
- @Override
- public void run() {
- try {
- reentrantLock();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public static void main(String[] args) throws InterruptedException {
- ObjOne one = new ObjOne();
- ObjTwo two = new ObjTwo();
- DeadlockDemo thread1 = new DeadlockDemo("Thread1", one, two);
- DeadlockDemo thread2 = new DeadlockDemo("Thread2", two, one);
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- }
- }
运行上面的 demo 将看到程序被阻塞了, 没法结束运行; 只看到如下运行结果:
Thread1 持有: objOne 对象锁, 等待获取: objTwo 对象锁 Thread2 持有: objTwo 对象锁, 等待获取: objOne 对象锁
Java 中死锁的定位与修复
这 demo 没法结束运行就是由于产生了死锁, 两个线程都在相互对待获取对方所持有的对象锁;
这时候要解决问题就需要找出哪里出现了死锁, 通过代码走查通常不容易发现死锁 , 当然我们这程序很容易发现, 因为我们刻意产生的死锁; 所以就需要工具来检测死锁, 这里可用的工具主要有: jconsole,jvisualvm,jstack 等, 这些工具其实都是 jdk 自带的, 用法都很类似;
这里使用 jvisualvm 来检测当前的 demo 程序是否产生了死锁; 打开 jvisualvm 连接到当前的应用程序即可看到程序的监控信息, 如内存, CPU, 性能, GC 等等; 打开进入线程的 tab 项查看程序的线程信息, 这里很明显的就看到了提示该程序被检测除了死锁!
Java 中死锁的定位与修复
点击 线程 Dump 可以看到线程的堆栈信息, 从中可以看到线程的详细信息, 并定位死锁;
Java 中死锁的定位与修复
从上图可以看到线程产生死锁的原因, Thrad2 是等待 Thread1,Thread1 是等待 Thread1, 从下图的堆栈信息即可定位死锁产生的位置;
Java 中死锁的定位与修复
死锁扫描
除了发现程序出现问题后我们去扫描死锁外, 我们还可以实时的去扫描程序用于发现程序中是否存在死锁;
JDK 提供了 MXBean API 可用于扫描程序是否存在死锁, ThreadMXBean 提供了 findDeadlockedThreads() 方法, 可以用于找到产生死锁的线程; 这里在上面的 demo 程序中添加一个方法用于扫描死锁, 虽然这种方法可以扫描到死锁但是由于每次都对线程打快照对程序性能会有比较大的影响, 所以慎用;
- public static void scanDeadLock() {
- ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
- Runnable runnable = () -> {
- long[] ids = mxBean.findDeadlockedThreads();
- System.out.println("扫描死锁...");
- if (ids != null) {
- ThreadInfo[] threadInfos = mxBean.getThreadInfo(ids);
- for (ThreadInfo threadInfo : threadInfos) {
- System.out.println(threadInfo);
- }
- }
- };
- ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(5, Executors.defaultThreadFactory());
- executorService.scheduleAtFixedRate(runnable, 1, 5, TimeUnit.SECONDS);
- }
Java 中死锁的定位与修复
避免死锁
解决死锁最好的方法就是避免死锁了, 比如上面的 demo 我们可以把直接使用无参数的 lock() 方法换为使用 tryLock 方法, tryLock 还可以指定获取锁超时时间, 到了超时时间还没获得到锁就会放弃获取锁, 当然还有其它方法可以避免死锁;
1, 避免使用多个锁, 长时间持有锁;
2, 设计好多个锁的获取顺序
3, 使用带超时的获取锁方法
Java 中死锁的定位与修复
来源: http://www.jianshu.com/p/eef78b62089b