避免活跃性危险:
- 本部分讨论活跃性故障的原因,及如何避免它们。
死锁:
- 典型的哲学家进餐问题。
锁顺序死锁:
如上面哲学家进餐有可能发生下面的情况:
- 上面发生死锁的根本原因在于两个线程以不同的顺序来获取相同的锁。
- 如果所有线程都以固定的顺序获取锁,那么程序就不会出现锁顺序死锁问题。
/**
* 容易因为获取锁的顺序导致死锁
*/
public class LeftRightDeadLock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight(){
synchronized(left){
synchronized(right){
// to do sth.
}
}
}
public void rightLeft(){
synchronized(right){
synchronized(left){
// to do sth.
}
}
}
}
动态的锁顺序死锁:
- 典型的就是银行转账问题
public void transferMoney(Account fromAccount, Account toAccount, int money){
synchronized (fromAccount) {
synchronized(toAccount){
if (fromAccount.getBalance() > money){
//余额不足
} else{
fromAccount.debit(money);
toAccount.credit(money);
}
}
}
}
当我们以下面这种方式调用,就有可能出现死锁:
transferMoney(a1, a2, money);
transferMoney(a2, a1, money);
要解决这种问题,就得使内部以相同的顺序加锁,无论外部怎么调用。
/**
* 用于当输入参数的hash值一样时使用
*/
private static final Object tieLock = new Object();
public static void transferMoney(final Account fromAccount,
final Account toAccount, final int money){
class Helper{
public void transfer(){
if (fromAccount.getBalance() < money){
//余额不足
} else{
fromAccount.debit(money);
toAccount.credit(money);
}
}
}
int fromHash = System.identityHashCode(fromAccount);
int toHash = System.identityHashCode(toAccount);
//无论客户端怎么传入参数,我们都以先锁定hash值小的,再锁定hash大的
//也可以利用业务中排序关系,如Account的编号等来比较
if (fromHash < toHash){
synchronized (fromAccount){
synchronized (toAccount) {
new Helper().transfer();
}
}
} else if (fromHash > toHash){
synchronized (toAccount){
synchronized (fromAccount) {
new Helper().transfer();
}
}
} else { //hash值相等, 情况很小
synchronized (tieLock) {
synchronized (fromAccount) {
synchronized (toAccount) {
new Helper().transfer();
}
}
}
}
}
在协作对象之间发生的死锁:
- 即在不同方法中相互持有等待得锁。
class Taxi {
private Point location;
private Point 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.notifyAvaliable(this);
}
}
}
class Dispatcher {
private final Set<Taxi> taxis;
private final Set<Taxi> avaliableTaxis;
public Dispatcher(){
taxis = new HashSet<>();
avaliableTaxis = new HashSet<>();
}
public synchronized void notifyAvaliable(Taxi taxi) {
avaliableTaxis.add(taxi);
}
public synchronized Image getImage(){
Image image = new Image();
for (Taxi t :taxis){
image.drawMarker(t.getLocation());
}
return image;
}
}
上面的
setLocation和
getImage就有可能发生死锁现象:setLocation获取到Taxi对象锁后,在dispacher.notifiyAvaliable()时需要dispatcher锁,而getImage获取到dispacher锁后,t.getLocation要求Taxi锁。
- 如果在持有锁时调用某个外部方法,那么将出现活跃性问题,在这个外部方法中可能会获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
开放调用:
- 如果在调用某个方法时不需要持有锁,那么这种调用就被称为开放调用(Open Call)。
/**
* 通过公开调用来避免在相互协作的对象之间产生死锁
*/
public class OpenCall {
class Taxi {
private Point location;
private Point destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation(){
return location;
}
public void setLocation(Point location){
boolean reachedDestination;
synchronized(this){
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination){
dispatcher.notifyAvaliable(this); //这里持有dispatcher锁,但已释放taxi锁
}
}
}
class Dispatcher {
private final Set<Taxi> taxis;
private final Set<Taxi> avaliableTaxis;
public Dispatcher(){
taxis = new HashSet<>();
avaliableTaxis = new HashSet<>();
}
public synchronized void notifyAvaliable(Taxi taxi) {
avaliableTaxis.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;
}
}
}
- 在程序中尽量使用开放调用。与那些在持有锁调用外部方法的程序时,更易于对依赖于开放调用的程序进行死锁分析。
资源死锁:
- 多个资源池(如数据库连接池),一个线程需要连接2个数据库连接,如线程A持有Pool1的连接,等待Pool2的连接;线程B持有Pool2的连接,等待Pool1的连接。
- 线程饥饿死锁,如一个任务中提交另一个任务,并一直等待被提交任务完成。
- 有界线程池/资源池与相互依赖的任务不能一起使用。
死锁的避免与诊断:
支持定时的锁:
- 限时等待。如Lock中的tryLock, 给定一个超时时限,若等待超过该时间,则会给出错误信息,避免永久等待。
通过线程转储信息来分析死锁:
- 可先通过jstack <pid>获取线程栈信息。
- 再通过分析工具Thread Dump Analyzer: https://java.net/projects/tda/downloads 进行死锁分析。
其他活跃性危险:
饥饿:
- 由于线程无法访问它所需的资源而不能继续执行时,就发生了"饥饿"。
- 要避免使用线程优先级,这会增加平台依赖性,并可能导致活跃性问题,在大多数并发应用程序中,都可以使用默认的线程优先级。
糟糕的响应性:
- 例如GUI程序中使用了后台线程。该后台任务若为CPU密集型,将可能影响程序响应性。
- 不良的锁管理也可能导致糟糕的响应性。
活锁:
- 活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。
- 当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。
- 在并发应用中,通过等待随机长度的时间和回退可以有效的避免活锁的发生。
不吝指正。