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. Android笔记——Bundle类的作用

    Bundle类用作携带数据,它类似于Map,用于存放key-value键值对形式的值.相对于Map,它提供了各种常用类型的putXxx()/getXxx()方法,如:putString()/getSt ...

  2. Ubuntu 14.04 中 安装elasticsearch2.*+logstash2.*+kibana

    在Ubuntu 14.04 上安装单机版ELK 2.*(脚本化) 1.判断是否为root权限 if [ "${UID}" -ne 0 ]; then echo "You ...

  3. 《JS设计模式笔记》 5,适配器模式

    //适配器模式的作用就像一个转接口. jQuery("#"+id); $id=function (id) { return jQuery("#"+id)[0]; ...

  4. .NET平台开源项目速览(1)SharpConfig配置文件读写组件

    在.NET平台日常开发中,读取配置文件是一个很常见的需求.以前都是使用System.Configuration.ConfigurationSettings来操作,这个说实话,搞起来比较费劲.不知道大家 ...

  5. KindEditor 给KindEditor赋值

    在项目的过程中,使用了KindEditor编辑器,然后在赋值的时候,我的编辑器ID是content,然后我想通过$("#content").html()来赋值,发现赋值失败,后来百 ...

  6. Javascript学习笔记1

    一.写在前面的话 之前一直没有系统的学习javascript,自己也是小白,很多东西感觉不会,从本篇起,自己会抽出时间来,慢慢学习,好好领会! 深知学习是一个漫长的过程,来不得急躁,不积跬步,无以至千 ...

  7. SQL Server 执行计划利用统计信息对数据行的预估原理二(为什么复合索引列顺序会影响到执行计划对数据行的预估)

    本文出处:http://www.cnblogs.com/wy123/p/6008477.html 关于统计信息对数据行数做预估,之前写过对非相关列(单独或者单独的索引列)进行预估时候的算法,参考这里. ...

  8. 深入理解javascript原生拖放

    × 目录 [1]拖放源 [2]拖放目标 [3]dataTransfer对象[4]改变光标 前面的话 拖放(drag-and-drop,DnD)其实是两个动作——拖和放.所以,它涉及到两个元素.一个是被 ...

  9. STM32F746的RTC使用

    1.RTC模块采用低速晶振外接始终:32.768KHz,如下图所示 2.配置RTC模块: 其中,Fck_apre.Fck_spre始终上配置不容易理解, 如果想得到1Hz的始终频率,则需要将PERDI ...

  10. 浅谈Static关键字

    1.使用static关键字声明的属性为全局属性 未使用static关键字指定city之前,如果需要将Tom,Jack,Mary三人的城市均改成Beijing,需要再次声明三次对象的city为Beiji ...