一、定义

  线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

当然死锁的产生是必须要满足一些特定条件的:
1.互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
2.请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

package com.sxy.thread;

/**
* 线程Thread1率先占有了resource1, 继续运行时需要resource2, 但此时resource2却被线程Thread2占有了,
* 因此只能等待Thread2释放resource2才能够继续运行; 同时,Thread2也需要resource1,
* 它只能等待Thread1释放resource1才能够继续运行, 因此,Thread1和Thread2都处于等待状态,
* 谁也无法继续运行,即产生了死锁。

*
* @author sunxy
*/
public class DeadLock {public static void main(String[] args) {
dead_lock();
}private static void dead_lock() {
// 两个资源
final Object resource1 = "resource1";
final Object resource2 = "resource2";
// 第一个线程,想先占有resource1,再尝试着占有resource2
Thread t1 = new Thread() {
public void run() {
// 尝试占有resource1
synchronized (resource1) {
// 成功占有resource1
System.out.println("Thread1 1:locked resource1");
// 休眠一段时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试占有resource2,如果不能占有,该线程会一直等到
synchronized (resource2) {
System.out.println("Thread1 1:locked resource2");
}
}
}
};
// 第二个线程,想先占有resource2,再占有resource1
Thread t2 = new Thread() {
public void run() {
// 尝试占有resource2
synchronized (resource2) {
// 成功占有resource2
System.out.println("Thread 2 :locked resource2");
// 休眠一段时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试占有resource1,如果不能占有,该线程会一直等到
synchronized (resource1) {
System.out.println("Thread1 2:locked resource1");
}
}
}
};
// 启动线程
t1.start();
t2.start();
}
}

死锁的另一种:递归死锁,举例:

所谓递归函数就是自调用函数,在函数体内直接或间接的调用自己,即函数的嵌套是函数本身。
递归方式有两种:直接递归和间接递归,直接递归就是在函数中出现调用函数本身。间接递归,指函数中调用了其他函数,而该其他函数又调用了本函数。
那什么时候使用递归呢?一般来说当你要在某段代码逻辑中使用循环迭代的时候但是迭代的次数在迭代之前无法知晓的情况下使用递归。打个比方你要在一个文件夹中查找某个文件,而这个文件夹底下有N多子文件夹和文件,当你在不知道有多少层文件夹和文件的情况下你就得用到递归了。
递归的优点就是让代码显得很简洁,同时有些应用场景不得不使用递归比如前面说的找文件。递归是个好东西但是在某些时候也会给你带来一些麻烦。比如在多线程的环境下使用递归,遇到了多线程那么就不得不面对同步的问题。而递归程序遇到同步的时候很容易出问题。多线程的递归就是指递归链中的某个方法由另外一个线程来操作,以下代码的意思都是这个意思即调用recursive()和businessLogic()并非一个线程(如果是在一个线程中就不存在死锁问题,例如下面的recursive变成private就不存在问题。)

[java] view plain copypublic class Test {  public void recursive(){  this.businessLogic();  }  public synchronized void businessLogic(){  System.out.println("处理业务逻辑");  System.out.println("保存到<a href="http://lib.csdn.net/base/mysql" class='replace_word' title="MySQL知识库" target='_blank' style='color:#df3434; font-weight:bold;'>数据库</a>");  this.recursive();  }
}  

以上这段代码就是个能形成死锁的代码,事实上这个“synchronized”放在“businessLogic()”和“recursive()”都会形成死锁,并且是多线程的情况下就会锁住!他的逻辑顺序是先执行recursive()方法然后接下来执行businessLogic()方法同时将businessLogic()方法锁住,接下来程序进入businessLogic()方法内部执行完打印语句后开始执行recursive(),进入recursive()后准备执行businessLogic(),等等问题来了!之前执行的businessLogic()的锁还没有放开这次又执行到这里了,当然是过不去的了,形成了死锁!从这个例子我们总结出来一个规律就是在递归的时候在递归链上面的方法上加锁肯定会出现死锁(所谓递归链就是指recursive()链向businessLogic(),businessLogic()又链回recursive()),解决这个问题的方法就是避免在递归链上加锁,请看以下的例子

[java] view plain copypublic class Test {  public void recursive(){  this.businessLogic();  }  public  void businessLogic(){  System.out.println("处理业务逻辑");  this.saveToDB();  this.recursive();  }  public synchronized void saveToDB(){  System.out.println("保存到数据库");  }
}

saveToDB()不在这条递归链上面自然不会出现死锁,所以说在递归中加锁是件很危险的事情,实在逃不过要加锁就加在最小的粒度的程序代码上以减小死锁的概率。

避免死锁:

在有些情况下死锁是可以避免的。本文将展示三种用于避免死锁的技术:

加锁顺序
加锁时限
死锁检测

加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。

如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个

Thread 1:
lock A
lock B

Thread 2:
wait for A
lock C (when A locked)

Thread 3:
wait for A
wait for B
wait for C

一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A
Thread 2 locks BThread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blockedThread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。

死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。

那么当检测出死锁时,这些线程该做些什么呢?

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。

一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

什么是死锁?如何避免死锁?相关推荐

  1. SQL Server-聚焦深入理解死锁以及避免死锁建议(三十三)

    前言 终于进入死锁系列,前面也提到过我一直对隔离级别和死锁以及如何避免死锁等问题模棱两可,所以才鼓起了重新学习SQL Server系列的勇气,本节我们来讲讲SQL Server中的死锁,看到许多文章都 ...

  2. python 线程死锁_python线程死锁与递归锁

    死锁现象 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去. 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待 ...

  3. 哲学家就餐与死锁问题,死锁产生的条件以及解决方案

    请结合经典案例-哲学家就餐,来谈谈你对死锁的理解,以及怎么预防和解除死锁? 哲学家就餐 描述:在一张圆桌上,有n个哲学家,n支筷子,他们的生活方式只是交替地进行思考和进餐,饥饿时便试图取其左.右最靠近 ...

  4. 在 C# 中,如何发现死锁并防止死锁

    在解释死锁如何发生以及如何阻止死锁的过程中,你似乎遇到了问题. 当两个( 最小二) 线程试图获取已经被另一个锁锁定的资源的锁时,就会发生死锁. 线程 1锁定资源 1尝试获取对资源 2的锁定. 同时,线 ...

  5. java 死锁 解决_Java死锁故障排除和解决

    java 死锁 解决 JavaOne年度会议的一大优点是主题专家介绍了几个技术和故障排除实验室. 这些实验室之一尤其引起了我的关注:Java冠军Heinz Kabutz提出的" HOL650 ...

  6. Oracle 制造死锁和查询死锁

    制造死锁 创造环境 create table t1_deadlock (a int); create table t2_deadlock (a int); insert into t1_deadloc ...

  7. 什么叫死锁?死锁案例?死锁必须满足哪些条件?如何定位死锁问题?有哪些解决死锁策略?哲学家问题?

    1.死锁是什么? 死锁一定发生在并发环境中,死锁是一种状态,当两个(或者多个线程)相互持有对方所需要的资源,却又都不主动释放手中持有的资源,导致大家都获取不到自己想要的资源,所有相关的线程无法继续执行 ...

  8. mysql加锁后怎么解除_Mysql查看死锁与解除死锁的深入讲解

    前言 前段时间遇到了一个Mysql 死锁相关的问题,整理一下. 问题描述:Mysql 的修改语句似乎都没有生效,同时使用Mysql GUI 工具编辑字段的值时会弹出异常. 什么是死锁 在解决Mysql ...

  9. 死锁的充分必要条件、死锁预防、死锁避免、死锁检测和解除

    2.19.2 死锁的条件 必要条件 互斥:一次只有一个进程可以使用一个资源 占有且等待:当进程等待其他资源时继续占有已有的资源 不可抢占:进程不能强行占有其他进程占有的进程 充分条件 循环等待:存在一 ...

  10. 死锁的处理策略—预防死锁、避免死锁、检测和解除死锁

    一.死锁的处理策略--预防死锁 (一)破坏互斥条件 互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁. 如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态.比如: SPOOLing ...

最新文章

  1. 【勘误】第4章字符串的实现
  2. SAP UI5 - MVC
  3. SolrJ管理SolrCloud
  4. (技能篇)Mysql在linux下的全量热备份
  5. 隐藏UITableView当没有数据或数据不够的时候出现的分割线.
  6. REVERSE-PRACTICE-BUUCTF-32
  7. 04-Fibonacci
  8. websocket的WebSocketServer无法注入service
  9. 深入剖析Android音频(二)AudioSystem
  10. Hadoop学习笔记(4)hadoop集群模式安装
  11. NOKIA PC 套件导入(import)CSV格式联系人列表
  12. ruby + watir 自动化上传图片文件解决方案
  13. RL78开发问题记录
  14. vs2012 +WP8sdk 开发WP8应用
  15. 软件推荐:16个你还没用过的优秀软件(多个类别)
  16. 原生js实现点击按钮显示/隐藏图片
  17. 3d智慧城市线上3d模型展示可视化平台
  18. 刘鹏:与聪明人一起做精彩的事!那么,谁是聪明人?
  19. 内蒙农信携手星环科技建设农信大数据平台,激活金融业务创新
  20. mac系统ssh可视化工具zoc的简单使用

热门文章

  1. GUNS框架图片上传详解
  2. 5、6月程序员“薪资被应届生倒挂“现象明显,跳槽还是等待?
  3. SpringBoot+OAuth2+JWT实现单点登录SSO完整教程,竟如此简单优雅!
  4. 什么是新零售电商系统?
  5. 希望计算机专业同学都知道这些宝藏博主
  6. php正则匹配preg_match,php正则表达式中preg_match函数的详解
  7. 数组实现链表(C++代码+图解)
  8. 如何将图片识别为可编辑的Word文件
  9. 半双工通信是指c语言,Linux下C语言实现半双工的UDP通信
  10. C语言关于有符号和无符号变量相互赋值的探讨