08避免活跃性危险

避免活跃性危险

锁是一把双刃剑,通过加锁来保证线程安全,但过度使用,则会导致死锁。

死锁

在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,线程AB均不会释放自己的锁,那么这两个线程将永远地等待下去

在数据库系统的设中考虑了检测死锁以及从死锁中恢复。JVM没有办法解决死锁,只能在编程和测试时注意不要让死锁发生

哲学家问题:

最简单的死锁形式:顺序死锁

锁顺序死锁

两个线程试图以不同的顺序来获得相同的锁

img

如何避免?

如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。

动态锁顺序死锁

1
2
1 A: transferMoney(myAccount, yourAccount, 10);
2 B: transferMoney(yourAccount, myAccount, 20);

如何避免?

使用某种顺序来控制获取锁的顺序(如比较hashCode适用于绝大多数情况)

协作对象之间发生死锁

如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(由于获取锁的顺序不同,可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。

如何避免?

尽量使用开放调用——在调用某个方法时不需要持有锁

资源死锁

两个线程分别持有彼此想要的资源而又不会释放

例:任务执行需要连接两个数据库,两个任务分别连接了其中一个数据库,而又等待彼此释放另一个数据库的资源

死锁的诊断和避免

如果一个线程每次至多只能获得一个锁,那么就不会产生锁顺序死锁。

如果必须获取多个锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入正式文档并始终遵循这些文档。

如何诊断是否会有死锁?

两阶段策略:

  1. 找出获取了多个锁的地方
  2. 判断获取锁的顺序

尽可能的使用开放调用会简化分析

支持定时的锁

显式使用Lock类中的定时tryLock功能来代替内置锁机制,显式锁则可以指定一个超时时限,在等待超过该时间后tryLock会返回一个失败信息

定时锁的局限?

当定时锁失败时,并不能确定是否由于死锁导致失败,也有可能是由于执行时间过长

通过线程转储信息进行分析

线程转储包括各个运行中的线程的栈追踪信息,这类似于发生异常时的栈追踪信息。线程转储还包括加锁信息,例如每个线程持有了哪些锁,在哪些栈帧中获得这些锁,以及被阻塞的线程正在等待获取哪一个锁。在生成线程转储之前,JVM将在等待关系图通过循环来找出死锁。如果发现了一个死锁,则获取相应的死锁信息,例如在死锁中涉及哪些锁和线程,以及这个锁的获取操作位于程序的哪些位置。

  显示锁比在内置锁上获得的信息精确度低。内置锁与获得它们所在的线程栈帧是相关联的,而显式的Lock只与获得它的线程相关联。

其他活跃性问题

饥饿

线程由于无法访问它所需要的资源时而不能继续执行

最常见的场景就是低优先级(任务、线程等)的程序由于无法获取CPU资源而无法执行

活锁

线程将不断重复执行相同的操作,而且总会失败(不会阻塞线程,但也不能继续执行)

案例:失败后一直重试

本文标题:08避免活跃性危险

文章作者:Sun

发布时间:2020年08月26日 - 20:08

最后更新:2020年08月26日 - 20:08

原始链接:https://sunyi720.github.io/2020/08/26/Java并发编程/08避免活跃性危险/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。