Java并发编程入门(九)死锁和死锁定位

Java极客  |  做者  /  铿然一叶
这是Java极客的第 37 篇原创文章

1、死锁条件

死锁:一组互相竞争资源的线程因互相等待,致使“永久”阻塞的现象。java

知足死锁的四个条件:
1.互斥,共享资源 X 和 Y 只能被一个线程占用
2.占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源Y的时候,不释放共享资源 X;
3.不可抢占,其余线程不能强行抢占线程 T1占有的资源,由于不可抢占,因此要等待;
4.循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。编程

这四个条件同时知足时,才会发生死锁,所以避免死锁只要打破其中一个条件则可。缓存

2、避免死锁方法

1.对于互斥这个条件没法破坏,由于使用锁为的就是互斥。
2.对于占有且等待,能够同时获取要使用的多个资源锁X和Y,这样就不会存在取得了X还要等待Y。这种方式只在须要获取的资源锁较少的状况下使用,若是要获取的资源锁不少(例如10个),就不太可行。
3.对于不可抢占,能够获取了部分资源,再进一步获取其余资源时若是获取不到时,把已经获取的资源一块儿释放掉。此时意味着操做不能按照预期处理,须要考虑异常如何处理,例如是否须要重试。
4.对于循环等待,能够将须要获取的锁资源排序,按照顺序获取,这样就不会多个线程交叉获取相同的资源致使死锁,而是在获取相同的资源时就等待,直到它释放。安全

综上,对于极易发生死锁的场景,处理以下:
1.获取锁时带上超时时间,获取不到就放弃,这样能最简单的避免死锁,这也意味着不能使用synchronized关键字来得到锁资源。
2.对于已经获取到的锁资源,增长主动释放机制。
3.放弃锁资源时增长异常流程处理,如重试。
4.须要获取的多个锁资源排序处理,虽然前面几点能够必定程度避免死锁,但不排序的结果就是首次处理失败,重试时还可能再次失败,虽然没有发生死锁,但同一笔业务重试了N次可能也没有成功,致使无谓占用资源。bash

3、死锁定位

1.模拟死锁代码并发

package com.javashizhan.concurrent.demo.deadlock;

/** * @ClassName DeadlockDemo * @Description TODO * @Author 铿然一叶 * @Date 2019/10/3 23:40 * javashizhan.com **/
public class DeadlockDemo {
    public static void main(String[] args) {
        //建立两个用于加锁的对象
        final Object lockX = new Object();
        final Object lockY = new Object();

        System.out.println("lockX " + lockX);
        System.out.println("lockY " + lockY);

        Thread tX = new Thread(new Worker(lockX, lockY), "tX");
        //交换锁的顺序,模拟死锁
        Thread tY = new Thread(new Worker(lockY, lockX), "tY");

        tX.start();
        tY.start();
    }
}

class Worker implements Runnable {

    private final Object lockX;

    private final Object lockY;

    public Worker(Object lockX, Object lockY) {
        this.lockX = lockX;
        this.lockY = lockY;
    }

    public void run() {
        synchronized (lockX) {
            //休眠一会,等待另一个线程获取到lockY
            sleep(2000);
            System.out.println(Thread.currentThread().getName() + " get lock " + lockX);

            synchronized (lockY) {
                //这一步因为发生了死锁永远不会执行
                System.out.println(Thread.currentThread().getName() + " get lock " + lockY);
            }
        }
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


复制代码

2.执行程序输出的日志post

lockX java.lang.Object@28d93b30
lockY java.lang.Object@1b6d3586
tX get lock java.lang.Object@28d93b30
tY get lock java.lang.Object@1b6d3586
复制代码

能够看到两个线程各自获取一个锁后发生了死锁,没有继续往下执行。优化

3.jps查看java进程this

4.jstack查看java进程堆栈信息,关键部分以下:spa


能够看到有一个死锁发生,缘由是tY线程和tX线程已经获取到的锁和将要获取的锁造成了循环依赖,致使死锁。

4、解决死锁问题

对于这个例子,死锁是由于两个锁循环依赖,根据上面描述的避免死锁方法,只要对锁排序则可,排序代码以下:

public Worker(Object lockX, Object lockY) {
        int result = lockX.toString().compareTo(lockY.toString());
        this.lockX = result == -1 ? lockX : lockY;
        this.lockY = result == -1 ? lockY : lockX;
    }
复制代码

代码修改后程序执行日志:

lockX java.lang.Object@28d93b30
lockY java.lang.Object@1b6d3586
tX get lock java.lang.Object@1b6d3586
tX get lock java.lang.Object@28d93b30
tY get lock java.lang.Object@1b6d3586
tY get lock java.lang.Object@28d93b30
复制代码

能够看到锁排序后,只有一个线程获取到全部锁并执行完后,另一个线程才能获取锁,死锁问题解决。

end.


相关阅读:
Java并发编程(一)知识地图
Java并发编程(二)原子性
Java并发编程(三)可见性
Java并发编程(四)有序性
Java并发编程(五)建立线程方式概览
Java并发编程入门(六)synchronized用法
Java并发编程入门(七)轻松理解wait和notify以及使用场景
Java并发编程入门(八)线程生命周期
Java并发编程入门(十)锁优化
Java并发编程入门(十一)限流场景和Spring限流器实现
Java并发编程入门(十二)生产者和消费者模式-代码模板
Java并发编程入门(十三)读写锁和缓存模板
Java并发编程入门(十四)CountDownLatch应用场景
Java并发编程入门(十五)CyclicBarrier应用场景
Java并发编程入门(十六)秒懂线程池差异
Java并发编程入门(十七)一图掌握线程经常使用类和接口
Java并发编程入门(十八)再论线程安全


Java极客站点: javageektour.com/

相关文章
相关标签/搜索