【Java并发】死锁

什么是死锁

  • 什么是死锁:
    发生在并发中,两者互不相让,互相持有对方所需要的资源,又不主动释放,导致程序卡死。

  • 死锁的影响
    死锁的影响在不同的系统中不同的。在数据库中就是可以检测并且放弃事务的。但是JVM无法自动处理死锁。

死锁发生的几率不高但是危害大,压力测试无法找出所有的潜在死锁。

发生死锁的条件

  1. 互斥条件
  2. 请求与保持条件
  3. 不剥夺条件
  4. 循环等地条件。

图解Java并发设计认为三点

  1. 存在多个资源块
  2. 线程持有某个资源块时还希望获得其他的资源块
  3. 获取资源的顺序并不固定。

死锁的修复

可以采用ThreadMXBean这一工具类。

        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if (deadlockedThreads != null && deadlockedThreads.length>0){
            for (int i = 0; i < deadlockedThreads.length; i++) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("find "+threadInfo.getThreadName());
            }
        }

实际工程中如何避免死锁

  • 线上问题都需要防患于未然,无法线上及时的修复
  • 首先需要保存现场,保存堆栈的信息,然后重启服务器等。首先保证用户的体验。
  • 暂时保证线上代码的安全, 利用保存的堆栈信息等,排查死锁,修改代码。

修复策略:

  1. 避免策略:哲学家就餐,设置一致的锁的获取顺序。
  2. 检测与恢复策略:每隔一段时间检测是否有死锁,如果有就剥夺某一个资源,打开死锁。
  3. 鸵鸟策略:直到发生死锁,再进行修复。

以哲学家就餐问题为例

参考

public class test {

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        int sum = 5;
        Chopstick[] chopsticks = new Chopstick[sum];
        for (int i = 0; i < sum; i++) {
            chopsticks[i] = new Chopstick();
        }
        for (int i = 0; i < sum; i++) {
            exec.execute(new Philosopher(chopsticks[i], chopsticks[(i + 1) % sum]));
        }
    }
}

// 筷子
class Chopstick {
    public Chopstick() {
    }
}
class Philosopher implements Runnable {

    private Chopstick left;
    private Chopstick right;

    public Philosopher(Chopstick left, Chopstick right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        try {
            while (true) {
                Thread.sleep(100);//思考一段时间
                System.out.println(Thread.currentThread().getName() + " " + "Think");
                synchronized (left) {
                    System.out.println(Thread.currentThread().getName() + " " + "left");
                    synchronized (right) {
                        System.out.println(Thread.currentThread().getName() + " " + "right");
                        Thread.sleep(1000);//进餐一段时间
                    }
                }
            }
        }
        catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

解决方案

  1. 服务员检查(避免策略)
  2. 改变一个哲学的拿起叉子的顺序, 给叉子进行编号,每次拿起小的。
  3. 餐票解决,最多n-1个餐票
  4. 检测与恢复策略,发生死锁之后检测到死锁,要求一个线程释放资源。
  • 检测与恢复策略:逐个终止线程,直到死锁消除;资源抢占。
    终止顺序:1. 优先级,前台交互还是后台处理;2. 已占用资源,还需要的资源。3. 运行时间。
    线程回退几步,但是可能导致线程饥饿。

有效避免的方法:

8个经验:

  • 设置超时时间:tryLock(1000,TimeUnit.MILLISECONDS)。synchronized无法尝试锁。获取锁失败再尝试发邮件,报警等。
  • 多使用并发类,不要自己设置锁。ConcurrentHashMap,ConcurrentLinkedQueue,AtomicBoolean
  • 尽量较低锁的使用粒度和临界区。
  • 如果可以使用同步代码块优先使用。
  • 给线程起有意义的名字。
  • 避免锁的嵌套。
  • 分配资源前考虑能不能收回,银行家算法
  • 不要几个功能用同一把锁。

其他活跃性问题

活锁

持有锁之后一段时间放开锁,再重新获得锁。可能正好还是卡死,编程活锁。
活锁的特点是线程没有被阻塞,但是没有进展。活锁还要持续消耗cpu资源。双方相互谦让导致谁都无法持有锁。

  • 引入随机重试,解决活锁问题。

饥饿

线程需要资源但是无法得到资源。
优先级的设置需要注意。

面试问题

  1. 实际中的死锁,线程需要持有多个锁的场合,锁的嵌套。
  2. 如何定位死锁,ThreadMXBean找到死锁。jstack类。
  3. 解决死锁的策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值