Java程序基本都要涉及到多线程,而在多线程环境中不可避免的要遇到线程死锁的问题。Java不像数据库那么能够检测到死锁,然后进行处理,Java中的死锁问题,只能通过程序员自己写代码时避免引入死锁的可能性来解决。

1. Java中导致死锁的原因

Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。

从这两个例子,我们可以得出结论,产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。另一个原因是默认的锁申请操作是阻塞的

2. Java中如何避免死锁

既然我们知道了产生死锁可能性的原因,那么就可以在编码时进行规避。Java是面向对象的编程语言,程序的最小单元是对象,对象封装了数据和操作,所以Java中的锁一般也是以对象为单位的,对象的内置锁保护对象中的数据的并发访问。所以如果我们能够避免在对象的同步方法中调用其它对象的同步方法,那么就可以避免死锁产生的可能性。如下所示的代码,就存在死锁的可能性:

public class ClassB {
private String address;
// ... public synchronized void method1(){
// do something
}
// ... ...
}
public class ClassA {
private int id;
private String name;
private ClassB b;
// ... public synchronized void m1(){
// do something
b.method1();
}
    // ... ...
}

上面的ClassA.m1()方法,在对象的同步方法中又调用了ClassB的同步方法method1(),所以存在死锁发生的可能性。我们可以修改如下,避免死锁:

public class ClassA {
private int id;
private String name;
private ClassB b;
// ... public void m2(){
synchronized(this){
// do something
}
b.method1();
}
// ... ...
}

这样的话减小了锁定的范围,两个锁的申请就没有发生交叉,避免了死锁的可能性,这是最理性的情况,因为锁没有发生交叉。但是有时是不允许我们这样做的。此时,如果只有ClassA中只有一个m1这样的方法,需要同时获得两个对象上的锁,并且不会将实例属性 b 溢出(return b;),而是将实例属性 b 封闭在对象中,那么也不会发生死锁。因为无法形成死锁的闭环。但是如果ClassA中有多个方法需要同时获得两个对象上的锁,那么这些方法就必须以相同的顺序获得锁。

比如银行转账的场景下,我们必须同时获得两个账户上的锁,才能进行操作,两个锁的申请必须发生交叉。这时我们也可以打破死锁的那个闭环,在涉及到要同时申请两个锁的方法中,总是以相同的顺序来申请锁,比如总是先申请 id 大的账户上的锁 ,然后再申请 id 小的账户上的锁,这样就无法形成导致死锁的那个闭环。

public class Account {
private int id; // 主键
private String name;
private double balance; public void transfer(Account from, Account to, double money){
if(from.getId() > to.getId()){
synchronized(from){
synchronized(to){
// transfer
}
}
}else{
synchronized(to){
synchronized(from){
// transfer
}
}
}
} public int getId() {
return id;
}
}

这样的话,即使发生了两个账户比如 id=1的和id=100的两个账户相互转账,因为不管是哪个线程先获得了id=100上的锁,另外一个线程都不会去获得id=1上的锁(因为他没有获得id=100上的锁),只能是哪个线程先获得id=100上的锁,哪个线程就先进行转账。这里除了使用id之外,如果没有类似id这样的属性可以比较,那么也可以使用对象的hashCode()的值来进行比较。

上面我们说到,死锁的另一个原因是默认的锁申请操作是阻塞的,所以如果我们不使用默认阻塞的锁,也是可以避免死锁的。我们可以使用ReentrantLock.tryLock()方法,在一个循环中,如果tryLock()返回失败,那么就释放以及获得的锁,并睡眠一小段时间。这样就打破了死锁的闭环。

比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1

此时如果T3申请锁L1失败,那么T3释放锁L3,并进行睡眠,那么T2就可以获得L3了,然后T2执行完之后释放L2, L3,所以T1也可以获得L2了执行完然后释放锁L1, L2,然后T3睡眠醒来,也可以获得L1, L3了。打破了死锁的闭环。

这些情况,都还是比较好处理的,因为它们都是相关的,我们很容易意识到这里有发生死锁的可能性,从而可以加以防备。很多情况的场景都不会很明显的让我们察觉到会存在发生死锁的可能性。所以我们还是要注意:

一旦我们在一个同步方法中,或者说在一个锁的保护的范围中,调用了其它对象的方法时,就要十而分的小心

1)如果其它对象的这个方法会消耗比较长的时间,那么就会导致锁被我们持有了很长的时间;

2)如果其它对象的这个方法是一个同步方法,那么就要注意避免发生死锁的可能性了;

最好是能够避免在一个同步方法中调用其它对象的延时方法和同步方法。如果不能避免,就要采取上面说到的编码技巧,打破死锁的闭环,防止死锁的发生。同时我们还可以尽量使用“不可变对象”来避免锁的使用,在某些情况下还可以避免对象的共享,比如 new 一个新的对象代替共享的对象,因为锁一般是对象上的,对象不相同了,也就可以避免死锁,另外尽量避免使用静态同步方法,因为静态同步相当于全局锁。还有一些封闭技术可以使用:比如堆栈封闭,线程封闭,ThreadLocal,这些技术可以减少对象的共享,也就减少了死锁的可能性。

总结一下

死锁的根本原因1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;2)默认的锁申请操作是阻塞的。所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。要采取各种方法来杜绝这种可能性。

Java多线程中的死锁问题的更多相关文章

  1. Java多线程中的死锁

    Java多线程中的死锁 死锁产生的原因 线程死锁是指由两个以上的线程互相持有对方所需要的资源,导致线程处于等待状态,无法往前执行. 当线程进入对象的synchronized代码块时,便占有了资源,直到 ...

  2. java多线程中的死锁情况读书笔记

    多线程中的死锁 在前面的分析中,我们知道一个对象可以用Synchronized方法或者其他的加锁形式来防止别的任务在互斥还没有释放的时候就访问这个对象. 试想一下这样的情况:某个任务在等待另一个任务, ...

  3. java多线程中的死锁、活锁、饥饿、无锁都是什么鬼?

    死锁.活锁.饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了. 死锁 死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又 ...

  4. Java 多线程中的死锁概述

    死锁 死锁的定义 发生在并发中 当两个线程(或更多)线程(或线程)相互持有对方所需要的资源,又不主动释放,导致所有线程都无法继续执行,是程序陷入无尽的阻塞,这就是死锁. 如果多个线程之间的依赖关系是环 ...

  5. Java多线程——线程的死锁

    Java多线程——线程的死锁 摘要:本文主要介绍了Java多线程中遇到的死锁问题. 部分内容来自以下博客: https://www.cnblogs.com/wy697495/p/9757982.htm ...

  6. java多线程中的三种特性

    java多线程中的三种特性 原子性(Atomicity) 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行. 如果一个操作时原子性的,那么多线程并 ...

  7. java 多线程中的wait方法的详解

    java多线程中的实现方式存在两种: 方式一:使用继承方式 例如: PersonTest extends Thread{ String name; public PersonTest(String n ...

  8. java多线程中并发集合和同步集合有哪些?区别是什么?

    java多线程中并发集合和同步集合有哪些? hashmap 是非同步的,故在多线程中是线程不安全的,不过也可以使用 同步类来进行包装: 包装类Collections.synchronizedMap() ...

  9. java多线程中最佳的实践方案是什么?

    java多线程中最佳的实践方案是什么? 给你的线程起个有意义的名字.这样可以方便找bug或追踪.OrderProcessor, QuoteProcessor or TradeProcessor 这种名 ...

随机推荐

  1. C++ std::array

    std::array template < class T, size_t N > class array; Code Example #include <iostream> ...

  2. innerHTML和innerText的区别

    以<p id="example">welcome to <strong>JavaScript</strong> !!!</p>为例: ...

  3. Android SearchView 自定义SearchIcon和字体颜色大小

    自定义SearchView的搜索图标和字体属性相对复杂一些,记下来. 一.自定义SearchIcon 1.API版本低于21:版本小于21时,要修改SearchIcon比较复杂,需要先获取到Searc ...

  4. 微信小程序开发调试工具

    为了帮助开发者简单和高效地开发微信小程序,我们推出了全新的 开发者工具 ,集成了开发调试.代码编辑及程序发布等功能. 扫码登录 启动工具时,开发者需要使用已在后台绑定成功的微信号扫描二维码登录,后续所 ...

  5. PHP 高级编程(4/5) - SPL异常类之 LogicException 逻辑异常

    SPL 提供了一系列标准异常.日常的使用中我们应该根据需求科学的使用它们,来使我们的程序更加健壮.LogicException 是从 Exception 基类派生的,没有添加任何附加方法.抛出逻辑异常 ...

  6. ASP.NET MVC5+EF6+EasyUI 后台管理系统(51)-系统升级

    系统很久没有更新内容了,期待已久的更新在今天发布了,最近花了2个月的时间每天一点点,从原有系统 MVC4+EF5+UNITY2.X+Quartz 2.0+easyui 1.3.4无缝接入 MVC5+E ...

  7. 从零开始编写自己的C#框架(12)——T4模板在逻辑层中的应用(一)(附源码)

    对于T4模板很多朋友都不太熟悉,它在项目开发中,会帮我们减轻很大的工作量,提升我们的开发效率,减少出错概率.所以学好T4模板的应用,对于开发人员来说是非常重要的. 园子里对于T4模板的介绍与资料已经太 ...

  8. J2EE 项目读写分离

    先回答下 1.为啥要读写分离? 大家都知道最初开始,一个项目对应一个数据库,基本是一对一的,但是由于后来用户及数据还有访问的急剧增多, 系统在数据的读写上出现了瓶颈,为了让提高效率,想读和写不相互影响 ...

  9. MUI开发APP,scroll组件,运用到区域滚动

    最近在开发APP的过程中,遇到一个问题,就是内容有一个固定的头部和底部.         头部就是我们常用的header了,底部的话,就放置一个button,用来提交页面数据或者进入下一个页面等,效果 ...

  10. android 视频录制 混淆打包 之native层 异常的解决

    原文地址:http://www.cnblogs.com/linguanh/    (滑至文章末,直接看解决方法) 问题起因: 前5天,因为项目里面有个类似 仿微信 视频录制的功能, 先是上网找了个 开 ...