思维导图:
引言:
本文的主要内容是介绍一些导致程序发生活跃性故障的原因,以及如何避免他们的方法。
- 理论部分:介绍死锁,活锁,饥饿这些活跃性故障发生的原因及避免方法。
一.死锁
有个经典的哲学家问题,有五个哲学家,每两个哲学家之间有一根筷子,当哲学家拥有两根筷子时就可以吃饭。这其中就有发生死锁的情况,即每个哲学家都占有一根筷子并等待其他人放下筷子,结果就是所有的哲学家都吃不着饭。
所以,什么情况下会产生死锁呢?当每个人都拥有其他人需要的资源,同时又等待其他人拥有的资源,并且每个人在获取所需要的资源之前又不肯放弃已拥有的资源,就会产生死锁。
接下来我们介绍部分死锁的类型以及如何避免发生死锁。
1.1 死锁类型
1.1.1 锁顺序死锁
由于多个线程不正确的获取锁的顺序而导致的死锁。比如如下例子,线程A执行leftRight方法可能已获取left的锁并等待获取right的锁。线程B则执行rightLeft方法并已获取right的锁并等待获取left的锁,此时,这两个线程就会发生死锁。
避免产生锁顺序死锁的方法则是将两个方法获取锁的顺序保持一致,都首先获取left的锁,或者都首先获取right的锁,就可以避免死锁的发生。
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();
}
}
}
void doSomething() {
}
void doSomethingElse() {
}
}
1.1.2 动态的锁顺序死锁
有时候,方法对锁的获取顺序并不是固定的,有可能是动态变化的。如下这个例子。当线程A将把账户1的钱转向账户2,同时线程B又将把账户2的钱转向账户1的话,就有可能发生死锁。
解决办法则是在方法内部对获取锁的方式进行固定。例如,每个账户一般都有一个唯一性的账号,我们可以通过账号的大小来对锁的获取进行排序,以避免动态的锁顺序死锁的发生。
public class DynamicOrderDeadlock {
// 可能发生死锁
public static void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
static class DollarAmount implements Comparable<DollarAmount> {
// Needs implementation
public DollarAmount(int amount) {
}
public DollarAmount add(DollarAmount d) {
return null;
}
public DollarAmount subtract(DollarAmount d) {
return null;
}
public int compareTo(DollarAmount dollarAmount) {
return 0;
}
}
static class Account {
private DollarAmount balance;
private final int acctNo;
private static final AtomicInteger sequence = new AtomicInteger();
public Account() {
acctNo = sequence.incrementAndGet();
}
void debit(DollarAmount d) {
balance = balance.subtract(d);
}
void credit(DollarAmount d) {
balance = balance.add(d);
}
DollarAmount getBalance() {
return balance;
}
int getAcctNo() {
return acctNo;
}
}
static class InsufficientFundsException extends Exception {
}
}
1.1.3 在协作对象之间发生死锁
死锁的发生并不会仅仅出现在同一个类的某个方法之中,就比如以上两个例子,而是有可能出现在多个类的多个方法的互相调用之中,比如以下这个例子。Taxi代表出租车,Dispatcher代表出租车车队。
线程A如果调用Taxe的setLocation方法的话,首先需要获取Taxi的锁,因为setLocation方法又使用了Dispatcher的notifyAvailable方法,所以还需要获取Dispatcher的锁。但是,如果此时,有个线程B,调用了Dispatcher的getImage方法,首先 会获取Dispatcher的锁,然后,在getImage方法之中,又会获取Taxi的锁,结果就是产生死锁。
public class CooperatingDeadlock {
// Warning: deadlock-prone!
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination)) {
dispatcher.notifyAvailable(this);
}
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
public synchronized Image getImage() {
Image image = new Image();
for (Taxi t : taxis)
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
避免在对象协作是产生死锁的方式则是使用开放调用,即在调用其他方法的时候不在持有某个对象的锁。如下例,在调用其他方法的时候,对持有的Taxi或Dispatcher的锁进行释放。
class CooperatingNoDeadlock {
@ThreadSafe
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
boolean reachedDestination;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination) {
dispatcher.notifyAvailable(this);
}
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
@ThreadSafe
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
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;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
1.1.4 资源死锁
举例,在某种比较极端的情况下。两个数据库连接池都只有一个连接可用。若线程A,B都需要使用这两个数据库连接池的唯一一个连接,而且获取连接的顺序不同的话,就有可能产生资源死锁。
线程饥饿死锁也是资源死锁的一种形式。例如,在单线程线程池中,正在执行的任务A需要任务B的结果,但是任务B又必须等待任务A执行完毕时才能执行,这就会导致线程饥饿死锁。
1.2 死锁的避免
在第一小节中已经介绍过一些避免死锁的方法,这一小节则进行总结,并添加一些额外的没有介绍到的方法以避免和诊断死锁。
- 加锁顺序固定:固定加锁的顺序以防止方法内死锁的发生
- 开放调用:在调用其他方法前释放已获取的锁,以避免死锁的发生
- 支持定时的锁:使用显示锁Lock并设置超时时间,以从死锁中恢复。
- 线程转储信息:可以利用线程转储信息分析死锁发生的原因
二.活锁
活锁的表现形式是不会阻塞线程,但也不能继续执行线程,因为线程将不断重复执行相同的操作,而且总是会失败。
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。活锁通常是由过度的错误恢复代码造成的。
解决活锁问题一般需要在重试时引入某种随机性。就好像在数据通信时,如果两台机器使用相同的载波来发送数据包,这些数据包就会发生冲突,如果这两个数据包都在固定的时间后尝试重新发送,那么肯定还是会发生冲突,但是如果这两个数据包都在一个有界限的随机的时间后尝试重新发送,那么冲突的可能性就基本没有了。
三.饥饿
当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿。
引发饥饿最常见的资源就是CPU时钟周期。如果Java优先级使用不当,或者在持有锁时执行一些无法结束的结构(例如无限循环,无限制的等待某个资源),就有可能导致饥饿的发生。
避免饥饿的方式是,不要尝试修改线程的优先级,使用默认优先级就好。修改线程优先级不仅可能会导致饥饿而且会增加平台的依赖性。