文档章节

【15】死锁的防范

秋雨霏霏
 秋雨霏霏
发布于 2017/09/09 22:42
字数 1665
阅读 48
收藏 0

加锁顺序

多线程发生死锁,主要是和他们加锁的顺序有关系。

如果确定所有的锁总是按照相同的顺序加锁,那么就不会遇到死锁。 来看看这个例子:

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这样,需要好几个锁,那就需要注意这几个锁的顺序。

例如,如果线程1先拿到锁A时,那线程2和线程3都不会拿到锁C。 这个时候,线程2和线程3其实都只能等待线程1释放锁A才能继续去申请锁B和锁C。

加锁顺序是预防死锁的一种简单而有效的机制。 然而,这是在你知道需要用到的全部锁的情况时,才能起作用的。 但实际中可能不是这样的。所以这个方法也并不是万能的。

加锁超时

另一个预防死锁的方法,就是在申请锁的时候,设置一个超时时间。 这样,就可以防止线程在某个得不到的锁上永久等待。 在超时时间之后,如果线程还得不到锁,那线程就可以返回加锁失败。 这样就可以根据这个返回信息,有机会来处理一些逻辑。 针对死锁,当然就可以在返回加锁失败时,就可以释放掉之前已经获得的锁。 这样就避免了其他线程长时间等待这些锁的情况发生了。 如此,就可以让程序避免死锁。

下面这个例子,展示了两个线程对两个锁以不同的顺序进行加锁,但是这次他们带有超时设定:

Thread 1 获得 锁A
Thread 2 获得 锁B

Thread 1 尝试获取 锁B 失败,进入阻塞等待
Thread 2 尝试获取 锁A 失败,进入阻塞等待

Thread 1 在 锁B 等待超时
Thread 1 返回,并释放已获得的 锁A
Thread 1 随机休息一定时间(例如:257 毫秒)后,进行重试.

Thread 2 在 锁A 等待超时
Thread 2 返回,并释放已获得的 锁B
Thread 2 随机休息一定时间(例如:43 毫秒)后,进行重试.

在上面这个例子中,线程2将会先于线程1大约200毫秒进行重试。 这样线程2就会有机会顺利的拿到两个锁。 而线程1将会重新尝试获取锁A。 当线程2完成后,线程1也会重试,并有机会顺利拿到两个锁。

但要记住一点,锁等待超时时间的设定,不代表就一定发生死锁了。 这个超时时间是避免长时间在某个锁上阻塞而设定的,比如如果持有锁的线程需要执行一个非常耗时的任务,这种情况下设置超时时间也能有效的避免长时间等待的情况,也可以避免线程饥饿的情况发生。

另外,如果过多线程对同一个资源进行争用,设置超时时间,就可能导致线程会不断的一次又一次的重试。 比如两个线程下超时时间在0~500毫秒的范围就可以正常运行,但是这不代表10或者20个线程的情况下也能正常。 线程太多,资源争用频繁的情况下,就可能导致两个或者多个线程容易在同一时间点进行重试。

超时机制还会有一个问题,那就是,Java的synchronized锁,是无法设置超时时间的。 这种情况下,就需要使用并发包(JUC)下的Lock类,进行显式的锁控制。

侦测死锁

侦测死锁是一个重要的死锁预防机制。 特别是在无法确定加锁顺序以及无法设置超时时间的情况下。

每次线程获得一个锁时候,就把这个线程和锁的对应信息记录到某个容器中(如:map)。 另外,在每次线程请求锁的时候,也记录到这个容器中。

当线程请求锁失败时,线程可以遍历这个容器,来检测是否发生死锁了。 例如,如果线程A请求锁7,但是锁7已经被线程B拿到,那么线程A就可以检查线程B是不是也在请求线程A已经持有的锁。如果线程B确实也在请求线程A的锁,那就可以侦测到死锁了。

当然,实际中的情况远比两个线程的情况要复杂。 可能在多个线程之间循环依赖,导致一个复杂的死锁。

下面这个图,就展示了一个侦测4个线程死锁的情况:

image

那么,侦测到死锁后,线程又需要做些什么呢?

一种策略是,释放掉所有已经获得的锁,并随机休息一段时间后再次进行重试。 这个策略有点类似超时时间的处理方式。所以,同样的,在争用激烈的情况下,频繁的重试成功率也不会太高。

另一个更好的策略是,在线程设进行重试时,设置一个优先级。这样,让优先级低的某几个(或者一个)放弃执行,进入等待。而其他线程,就像没有发生死锁那样继续执行。这样,就可以避免重试再次失败的情况。

不过要注意,如果线程的优先级是固定的,那可能导致某些高优先级的线程一直得到处理,低优先级的线程一直得不到执行机会。为了避免这种情况的发生,可以在侦测到死锁的时候,随机分配优先级。


补充几点

避免死锁

  • 一次至多获得一个锁
  • 尽可能使用开放调用
  • 使用显示Lock类,替代内部锁机制
    • tryLock
    • timeout

有关加锁顺序的注意事项,也同样适用于数据库操作。 如,多个线程可能对某些数据执行update操作,这个时候,如果update的数据行,也是按不同顺序update,那也同样容易导致数据库死锁。

所以,在调用批量update时,最好对数据集合以相同的规则进行一次排序。

© 著作权归作者所有

共有 人打赏支持
秋雨霏霏
粉丝 150
博文 94
码字总数 168411
作品 0
杭州
CTO(技术副总裁)
私信 提问
稍微有点难度的10道java面试题,你会几道?

1、jvm对频繁调用的方法做了哪些优化? 2、常见的攻击手段有哪些?如何防范? 3、restful api有哪些设计原则? 4、hessian是做什么用的?它的传输单位是什么? 5、http中的post、get有什么区...

java技术栈
2017/08/13
0
0
程序员的自我修养——操作系统篇

目录: 1. 进程的有哪几种状态,状态转换图,及导致转换的事件。 2. 进程与线程的区别。 3. 进程通信的几种方式。 4. 线程同步几种方式。 5. 线程的实现方式. (用户线程与内核线程的区别) 6...

马浩
2014/06/30
0
0
WSFC资源死锁案例

之前在WSFC日志分析进阶篇中曾经提到过一些关于WSFC底层原理,例如Resource.dll,RHS,RCM,了解这些组件对于我们后期做群集排错有莫大的帮助,本文我们就通过一个实际的资源死锁的案例,来帮...

老收藏家
2017/10/20
0
0
高并发Java(5):JDK并发包1

在高并发Java(2):多线程基础中,我们已经初步提到了基本的线程同步操作。这次要提到的是在并发包中的同步控制工具。 1. 各种同步控制工具的使用 1.1 ReentrantLock ReentrantLock感觉上是...

卯金刀GG
2017/11/02
0
0
3.15热话题:电商网银系统如何打击假冒钓鱼网站,提高在线信任!

  一年一度的3.15国际消费者权益日即将到来,产品质量、售后服务、虚假宣传等消费者权益保护问题再次成为社会关注焦点。然而,在互联网高速发展的今天,人们大部分消费行为都已经转移到互联...

sslor
2016/03/14
48
0

没有更多内容

加载失败,请刷新页面

加载更多

css hack

浏览器的兼容性一直是个头疼的问题,使用“欺骗”技术可使各个浏览器效果一致,花了些时间整理了各个浏览器的HACK,主要包括IE系列和最新版本的Chrome、Safari、Firefox、 Opera,比较全面的...

kitty1116
14分钟前
0
0
zookeeper脑裂问题

一、为什么zookeeper要部署基数台服务器? 二、zookeeper脑裂(Split-Brain)问题 2.1、什么是脑裂? 2.2、什么原因导致的? 2.2、zookeeper是如何解决的? 一、为什么zookeeper要部署基数台...

tantexian
20分钟前
1
0
Spring事务传播行为详解

前言 Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。事务传播行为是Spring框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。这是Spring为我们提供的强大的...

码代码的小司机
30分钟前
2
0
Android设备通过fastboot刷入TWRP

方法一:通过fastboot刷入TWRP的方式 首先去TWRP官网下载TWRP安装文件https://twrp.me/Devices/ 1.进入bootloader adb reboot bootloader 也可在开机时,同时按住电源键+音量减,进入bootloa...

robslove
35分钟前
0
0
为何译为“东家机”和“宾客机”

学习过虚拟化、云计算的人大概都知道,并且都在自己的电脑上安装KVM,如下图所示: 什么情况?不过是在物理机的Windows 10上安装了VMware;在VMware上安装了Linux CentOS 7操作系统;又在其上...

大别阿郎
49分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部