顺序死锁与资源死锁 - 我们使用加锁机制来确保线程安全,但是如果过度的使用加锁,则可能导致锁顺序死锁。同样,我们使用线程池和信号量来限制对资源的使用,但这些限制的行为可能导致资源死锁。
在数据库系统的设计中考虑了监测死锁以及从死锁中恢复。当执行一个事务时可能需要获取多个锁,并一直持有这些锁直到事务提交。因此两个事务之间很可能发生死锁。当发生死锁的时候,数据库服务器将选择一个牺牲者并放弃这个事务。作为牺牲者的事务将放弃它所持有的资源,从而让其他事务继续执行。
JVM在解决死锁问题方面并没有数据库服务器那样强大。当一组Java线程发生死锁时,“游戏”将到此结束 - 这些线程永远不能再使用。恢复应用程序的唯一方式就是中止并重启它,并希望不要再发生同样的事情。
1、锁顺序死锁
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();
}
}
}
}
两个线程分别调用上面的两个方法就会发生死锁。如果所有的线程都以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。
2、动态的锁顺序死锁
public void transferMoney(Account fromAccount,Account toAccount,DollarAmount amount){
synchronized (fromAccount){
synchronized (toAccount){
if (fromAccount.getBalance().compareTo(amount) < 0){
throw new InsufficientFundsException();
}else{
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
上述看似无害的代码,有的时候也会发生死锁。为什么?所有的线程都按照相同的顺序来获得锁,但事实上锁的顺序取决于传递给该方法的参数,而这些参数顺序又取决于外部输入。如果两个线程同时调用这个方法,其中一个传入从X向Y转账,而另一个线程从Y向X转账,那么就会发生死锁。要解决这个问题,必须定义锁的顺序,并在整个程序中都按照这个顺序来获取锁。制定锁顺序的时候,可以使用System.identityHashCode,该方法将返回有object.hashCode返回的值。
public void transferMoney(Account fromAccount,Account toAccount,DollarAmount amount){
int fromHash = System.identityHashCode(fromAccount);
int toHash = System.identityHashCode(toAccount);
if (fromHash<toHash){
synchronized (fromAccount){
synchronized (toAccount) {
......
}
}
}else if(fromHash>toHash){
synchronized (toAccount){
synchronized (fromAccount) {
......
}
}
}else{
synchronized (tiedLock){
synchronized (fromAccount){
synchronized (toAccount) {
......
}
}
}
}
}
在极少数情况下,两个对象可能拥有相同的散列值,为了避免这种情况,可以使用“加时赛”锁。在获取两个锁之前,先获得这个“加时锁”,从而保证每次只有一个线程以未知的顺序获得这两个锁,从而消除死锁的发生。
3、在协作对象之间发生的死锁
class Taxi {
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);
}
}
class Dispatcher{
private final Set<Taxi> taxis;
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())l
return image;
}
}
}
【如果在持有锁时调用某个外部方法,而这个外部方法可能需要其他锁,这就可能造成死锁】
尽管没有任何方法会显式地获取两个锁。但setLocation和getImage等方法的调用者都会获得两个锁。调用setLocation会首先获得Taxi的锁,然后获取Dispatcher的锁。同样,调用getImage会首先获得Dispatcher的锁,然后再获取Taxi的锁。如果一个线程调用了setLocation,而另一个线程调用了getImage,两个线程按照不同的顺序获取锁,因此就可能发生死锁。解决方式:开放调用。如果调用某个方法时不需要持有锁,那么这种调用被称为开放调用。依赖于开放调用的类通常能表现出更好的行为,并且与那些在调用方法时需要持有锁的类相比,也更易于编写。
上述代码通过“开放调用 + 同步代码块【保护那些涉及共享状态的操作】”即可解决。 - 解除了持有锁时调用外部方法
class Taxi {
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);
}
class Dispatcher{
private final Set<Taxi> taxis;
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(){
Set<Taxi> copy;
synchronized (this){
copy = new HashSet<>(taxis);
}
Image image = new Image();
for (Taxi t : taxis){
image.drawMarker(t.getLocation());//此时也不再持有锁
return image;
}
}
}
}
在程序中应尽量使用开放调用,与那些在持有锁时调用外部方法的程序相比,更容易对依赖于开放调用的程序进行死锁分析。
4、资源死锁
当多个线程在相同的资源集合上等待时,也会发生死锁。有界线程池/资源池不能与相互依赖的任务一起使用。5、支持定时的锁
还有一项技术可以检测死锁和从死锁中恢复过来:Lock,显式地使用Lock类中的定时tryLock功能来代替内置锁。当使用内置锁时,只要没有获取锁,就会永远等待下去,而显式锁则可以指定一个超时时限,在等待超过该时间后tryLock会返回一个失败信息。
还有一个问题就是避免使用线程的优先级,因为这会增加平台依赖性,并导致出现活跃性问题。在Thread Api中定义了10个优先级,JVM根据需要将它们映射到操作系统的调度优先级。这种映射与特定的平台有关,因此在某个操作系统中两个不同的Java优先级可能被映射到同一个优先级,而在另一个操作系统中则可能被映射到另一个不同的优先级。在某些操作系统中,如果优先级的数量少于10,那么多个Java优先级会被映射到同一个优先级。
通常,我们尽量不要改变线程的优先级,只要改变了线程的优先级,程序的行为就与平台相关,并且会导致发生饥饿问题。在大多数应用程序中,所有的线程都具有相同的优先级Thread.NORMAL。