死锁

来源:互联网 发布:nginx 添加ip黑名单 编辑:程序博客网 时间:2024/06/03 19:33

当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。在线程A持有锁L并想获得锁M时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去。

1. 顺序死锁

下列的程序清单简单的说明了顺序死锁,leftRight和rightLeft这两个方法分别获得left锁和right锁。如果一个线程调用了leftRight,而另一个线程调用了rightLeft,并且这两个线程的操作是交错执行,那么它们就会发生死锁。

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){                //doSomething();            }        }    }}

2. 动态的锁顺序死锁

有时候,并不能清除地知道是否在锁顺序上有足够的控制权来避免死锁的发生。下列的程序简单的展示了资金从一个账户转入另一个账户。在开始转账之前,首先要获得这两个Account对象的锁,以确保通过原子方式来更新账户中的余额。如果两个线程同时调用transferMoney,其中一个线程从A向B转账,另一个线程从B向A转账,那么就会发生死锁。

public void transferMoney(Account fromAccount,                           Account toAccount,                           DollarAmount amount){    synchronized(fromAccount){        synchronized(toAccount){            fromAccount.debit(amount);            toAccount.credit(amount);        }    }}

3. 通过锁顺序来避免死锁

上述的死锁可以通过限制锁的顺序来避免,程序清单如下所示。在极少的情况下,两个对象可能拥有相同的散列值,此时必须通过某种任意的方法来决定锁的顺序,而这可能又会重新引入死锁,为了避免这种情况,可以使用“加时赛(Tie-Breaking)”锁。在获得两个Account锁之前,首先要获得这个“加时赛”锁,从而保证每次只有一个线程以未知的顺序来获得这两个锁,从而消除了死锁发生的可能性。

private static final Object tieLock = new Object();public void transferMoney(final Account fromAccount,                          final Account toAccount,                          fianl DollarAmount amount){    class Helper{        public void transfer(){            fromAccount.debit(amount);            toAccount.credit(amount);        }    }    int fromHash = System.identityHashCode(fromAccount);    int toHash = System.identityHashCode(toAccount);    if(fromHash < toHash){        synchronized(fromAccount){            synchronized(toAccount){                new Helper.transfer();            }        }    }else if(fromHash > toHash){        synchronized(toAccount){            synchronized(fromAccount){                new Helper.transfer();            }        }    }else{        synchronized(lieLock){            synchronized(fromLock){                synchronized(toLock){                    new Helper.transfer();                }            }        }    }                     }

4. 在协作对象之间发生的死锁

某些获取多个锁的操作不会很明显,这两个锁并不一定在同一个方法中获取。如下列程序所示,两个相互协作的类,在出租车调度系统中可能会用到它们。Taxi代表一个出租车对象,包含位置和目的两个属性,Dispatcher代表一个出租车车队。

class Taxi{    private Point location, destination;    private final Dispatcher dispatcher;    public Taxi{        this.dispatcher = dispatcher;    }    public synchronized Point getLocation(){        return location;    }    public synchronized void setLocation(){        this.location = location;        if(location.equals(destination){            dispatcher.notifyAvailable(this);        }    }}class Dispatcher{    private final Set<Taxi> taxis;    private fianl 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;    }}

尽管没有任何方法会显示地获取两个锁,但setLocation和getImage等方法的调用者都会获得两个锁。如果一个线程在收到GPS接收器的更新时间时调用setLocation,那么它将首先更新出租车的为主子,然后判断它是否到达了目的地,如果已经到达,它会通知Dispatcher,它需要更新一个新的目的地。因为setLocation和notifyAvailable都是同步方法,因此调用setLocation的线程将首先获取Taxi的锁,然后获取Dispatcher的锁。同样,调用getImage的线程首先获得Dispatcher的锁,然后再获取每个Taxi的锁。这与LeftRightDeadLock中的情况相同。两个线程按照不同的顺序来获取锁,有可能会造成死锁。

5. 开放调用

如果在调用某个方法不需要持有锁,那么这种调用被称为开放调用(OPEN CALL)。依赖于开发调用的类通常能表现出更好的行为,并且与那么在调用方法时需要持有锁的类相比,也更容易编写。这种通过开放调用来避免死锁的方法,类似于采用封装机制来提供线程安全的方法。

可以很容易的将程序清单中的Taxi和Dispatcher修改为使用开发调用。从而消除死锁的风险。这需要使同步代码块仅用于保护哪些涉及共享状态的操作。

class Taxi{    private Point location, destination;    private final Dispatcher dispatcher;    public Taxi{        this.dispatcher = dispatcher;    }    public synchronized Point getLocation(){        return location;    }    public void setLocation(){        boolean reachedDestination;        synchronized(this){            this.location = location;            reachedDestination = location.equals(destination);        }        if(reachedDestination){            dispatcher.notifyAvailable(this);        }    }}class Dispatcher{    private final Set<Taxi> taxis;    private fianl 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();        Set<Taxi> copy;         synchronized(this){            copy = new HashSet<Taxi>(taxis);        }        for(Taxi t : copy){            image.drawMarker(t.getLocation());        }        return image;    }}

6. 资源死锁

假设有两个资源池,例如两个不同数据库的连接池,资源池通常采用信号量来实现。如果一个任务需要连接两个数据库,并且请求这两个不会始终遵循相同的顺序,那么线程A可能持有与数据库D1的连接,并等待与数据库D2的连接。线程B则持有D2的连接,并等待与数据库D1的连接(资源池越大,出现这种情况的可能性越小,如果每个资源池有N个连接,那么在发生死锁时不仅需要N个循环等待的线程,而且还需要大量不恰当的执行时序)。

另一种资源死锁的形式是线程饥饿死锁。一个任务提交另一个任务,并等待提交的任务在单线程的Executor中执行完成,这种情况下,第一个任务将永远等待下去,并使得另一个任务以及在这个Executor中执行的所有其他任务都停止执行。

7. 活锁

活锁通常发生在处理事务消息的应用程序中:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务。并将它重新放到队列的开头,如果消息处理器在处理某个特定类型的消息时存在错误并导致它失败,那么每当这个消息从队列中取出并传递到错误的处理器时,都会发生事务回滚。由于这条消息被放回到队列开头,因此处理器被反复调用,并返回相同的结果。谁然处理消息的线程并没有阻塞,但也无法继续执行下去。

原创粉丝点击