第10章 避免活跃性危险
在安全性与活跃性之间通常存在着某种制衡。我们使用加锁机制来确保钱程安全,但如果过度地使用如锁,则可能导致顺序死锁( Lock-Ordering Deadlock )。同意,我们使用线程和信号量来限制对资源的使用,但这些被限制的行为可能会导致资源死锁(Resource Deadlock )。
死锁
经典的“哲学家进餐”问题很好的描述了死锁状况。
当死锁出现时,往往是最糟糕的时候--高负载的情况下
锁顺序死锁
在LeftRightDeadLock中发生死锁的原因是:两个线程试图以不同的顺序来获得相同的锁。
public class LeftRightDeadLock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
synchronized (left) {
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
synchronized (right) {
synchronized (left) {
doSomethingElse();
}
}
}
}
如果所有线程以固定的顺序来获取锁,那么在程序中就不会出现顺序死锁的问题。
动态的锁顺序死锁
public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsuffcientFundsException {
synchronized (fromAccount) {
synchronized (toAccount) {
}
}
}
在协作对象之间发生的死锁
如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(可能会死锁)或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
开放调用
在调用某个方法时不需要持有锁,那么这种调用将被称为开放调用(Open Call)
@ThreadSafe
class Taxi {
@GuardedBy("this")
private Point location, destination;
private final Dispatcher dispatcher;
public synchronized Ponit getLocation() {
return location;
}
public void setLocation(Point location) {
boolean reachedDestination;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination) {
dispatcher.notifyAvailable(this);
}
}
}
@ThreadSafe
class Dispatcher {
@GuardedBy("this")
private final Set<Taxi> taxis;
@GuardedBy("this")
private final Set<Taxi> availableTaxis;
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
public Image getImage() {
set<Taxi> copy;
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
Image image = new Image();
for (Taxi t : copy) {
image.drawMarker(t.getLocation());
}
return image;
}
}
在程序中尽量使用开放调用。与那些在持有锁时调用外部方法的程序相比,更易于对依赖于开放调用的程序进行死锁分析
资源死锁
正如当多个结程相互持有彼此正在等待的锁又不释放自己己持有的锁时会发生死锁,当它们在相同的资源集合上等待时,也会发生死锁。
如两个不同数据库的连接池,资源池通常采用信号量来实现当资源池为空时的阻塞行为。如果一个任务需要连接两个数据库,并且在请求这两个资源时不会始终遵循相同的顺序,那线程A持有D1 的连接,等待 D2连接。B则持有D2,等待D1。
另一种是线程饥饿死锁(Thread-Stravation Deadlock) 一个任务提交另一个任务,并等待被提交任务在单线程的的 Executor中执行完成。这种情况下,第一个任务将来远等待下去,并使得另一个任务以及在这个 Executor中执行的所有其他任务都停止执行。如果某些任务需要等待其它任务的结果,那么这拴任务往往是产生线程饥饿死锁的主要来源,有界线程池/资源池与相互依赖的任务不能一起使用。
死锁的避免与诊断
支持定时的锁
当使用内置锁时,只要没有获得锁,就会永远等待下去,而显示锁则可以指定一个超时时限( Timeout),在等待超过读时间后 tryLock 会返回一个失败信息。
通过线程转储信息来分析死锁
JVM 仍然通过线程转储(Thread Dump)来帮助识别死锁的发生
其他活跃性危险
饥饿
当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿( Starvation)
引发饥饿的最常见资源就是 CPU 时钟周期。如果在 Java 应用程序中对线程的优先级使用不当,
或者在持有锁时执行一段无法结束的结构(例如无限锚坏,或者无限制地等待某个资源),那
么也可能导致饥饿。
要避免使用线程的优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。
糟糕的响应性
CPU密集型的后台任务可能对响应性造成影响。
不良的锁管理也可能导致糟糕的响应性。
活锁
活锁(Livelock)是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。
毒药消息(Poison Message)过度的错误恢复代码造成
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。。这就像两个过于礼貌的人在半路上面对面地相遇:他们按此都让出对方的路,然而又在另一条路上相遇了。因此他们就这样反复地避让下去。
要解决这种活锁问题,需要在重试机制中引入随机性。