避免活跃性
10 避免活跃性
在安全性与活跃性之间通常存在着某种制衡。例如加锁导致死锁,或者使用线程池和信号量来限制对资源的使用,但这些被限制的行为可能会导致资源死锁。、
10.1 死锁
当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。
10.1.1 锁顺序死锁
如果用固定的顺序来获取锁,就不会发生死锁。
10.1.2动态的锁顺序死锁解决方案
其中一个线程从X向Y转账,另一个线程从Y向X转账,就会发生死锁:
A:transferMoney(myACCOUnt,yourACCOUnt,10)
B:transferMoney(yourACCOUnt,myACCOUnt,20)
于我们无法控制参数的顺序,因此要解决这个问题,必须定义锁的顺序。在制定锁的顺序时,可以使用System.identityHashCode方法,该方法将返回由Object. hashCode返回的值。如果返回的hash值相同(小概率),则采用加时赛锁tieLock
10.1.3开放调用
什么是协作对象之间的死锁?
在持有锁的情况下,调用外部(不了解)的方法,容易出现活跃性问题。在这个外部方法中可能会出现其他锁,或者长时间阻塞,导致当前持有的锁不能被其他线程获得。
用例:
因为setLocation和notifyAvailable都是同步方法,因此调用setL.ocation的线程将首先获取Taxi的锁,然后获取Dispatcher的锁。同样,调用getImage的线程将首先获取Dispatcher锁。然后再获取每一个Taxi的锁(每次获取一个)。产生死锁。
什么是开放调用原则?
即在调用某个方法时不需要持有锁。这样可以避免协作死锁,编码和分析安全性都变得简单
10.1.4 资源死锁
如果一个任务需要连接两个数据库。那么线程A可能持有与数据库D1的连接,并等待与数据库D2的连接,而线程B则持有与D2的连接并等待与D1的连接。
另一种基于资源的死锁形式就是线程饥饿死锁。如果某些任务需要等待其他任务的结果,那么这些任务往往是产生线程饥饿死锁的主要来源,所以有界线程池/资源池与相互依赖的任务不能一起使用。
10.2 死锁的避免与诊断
10.2.1 基本方式
A:将锁的使用顺序写入文档,避免死锁
B:可以通过代码审查,或者借助自动化的源代码分析工具检查死锁
C:遵循开放调用原则
10.2.2 显示锁的定时功能
使用显示锁Lock的定时功能(tryLock)代替内置锁机制。当使用内置锁时,只要没有获得锁,就会永远等待下去,而显式锁则可以指定一个超时时限(Timeout),在等待超过该时间后tryLock会返回一个失败信息。当定时锁失败时,你并不需要知道失败的原因。但是你记录了这次操作的其他有用信息。并通过一种更平缓的方式来重新启动计算,而不是关闭整个进程。
即使在整个系统中没有始终使用定时锁,使用定时锁来获取多个锁也能有效地应对死锁问题。如果在获取锁时超时,那么可以释放这个锁,然后后退并在一段时间后再次尝试,从而消除了死锁发生的条件,使程序恢复过来。(这项技术只有在同时获取两个锁时才有效,如果在嵌套的方法调用中请求多个锁,那么即使你知道已经持有了外层的锁,也无法释放它。)
10.2.3 线程转储
线程转储包括各个运行中的线程的栈追踪信息,这类似于发生异常时的栈追踪信息。例如每个线程持有了哪些锁,在哪些栈帧中获得这些锁,以及被阻塞的线程正在等待获取哪一个锁。在生成线程转储之前,JVM将在等待关系图中通过搜索循环来找出死锁。
虽然Java 6中包含对显式Lock的线程转储和死锁检测等的支持,但在这些锁上获得的信息比在内置锁上获得的信息精确度低。内置锁与获得它们所在的线程栈帧是相关联的,而显式的Lock只与获得它的线程相关联。
10.3 其他活跃性危险
10.3.1 慎用thread的优先级
JVM根据需要将thread线程的优先级映射到操作系统的调度优先级。但是在Thread API中定义10个优先级,不同的操作系统的优先级却不同,可能少于10个,这样就造成不同优先级的线程映射为相同的优先级,使线程的优先级失去了意义。所以慎用thread优先级,尽量用Thread.sleep或Thread.yield。
10.3.2糟糕的响应性
CPU密集型的后台任务仍然可能对响应性造成影响,因为它们会与事件线程共同竞争CPU的时钟周期。
10.3.3 活锁
要解决这种活锁问题,需要在重试机制中引人随机性。
例如,在网络上,如果两台机器尝试使用相同的载波来发送数据包,那么这些数据包就会发生冲突。这两台机器都检查到了冲突,并都在稍后再次重发。如果二者都选择了在1秒钟后重试,那么它们又会发生冲突,并且不断地冲突下去,因而即使有大量闲置的带宽,也无法使数据包发送出去。为了避免这种情况发生,需要让它们分别等待一段随机的时间。