动态的锁顺序死锁
这是接着上一篇的写。为了方便又贴了一遍代码,因为第二天我发现,这个通过锁顺序来避免死锁的程序依旧有问题。
我的问题是:
一个对象的Object的Hashcode的值 应该是固定的,那么。虽然这个代码通过hashcode规范了锁顺序,当两个人互相往对方的账户里面转账的时候。不还是变成了
public void transferMoney(Account formaccaount, Account toaccount, DollarAmount amount) { synchronized (formaccaount) { synchronized (toaccount) { if (formaccaount.getBalance.compareTo(amount) < 0) { throw new InsufficientFundsExcetion(); } else { fromaccount.debit(amount); toaccount.credit(amount); } } } }
这样的代码。因为还是Fromacct和toacct互换了一个位置啊。后来仔细一想,哈哈确实想少了,因为Fromacct和toacct互换了位置,那么其实里面的嵌套锁也换啦,虽然名字上看来不一样,但是如果两个人都互相往对方转账的话,锁的顺序是一样的。
public class TestLock { private static final Object tieLock = new Object(); private Account fromacct; private Account toacct; public void transferMoney(final Account fromacct, final Account toacct, final DollarAmount amount) { this.fromacct = fromacct; this.toacct = toacct; class Helper { public void transfer() { if (fromacct.getBalance().compareTo(amount) < 0) { throw new InsufficientFundsExcetion(); } else { fromacct.debit(amount); toacct.credit(amount); } } } int fromHash = System.identityHashCode(fromacct); int toHash = System.identityHashCode(toacct); if (fromHash < toHash) { synchronized (fromacct) { synchronized (toacct) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toacct) { synchronized (fromacct) { new Helper().transfer(); } } } else { synchronized (tieLock) { synchronized (fromacct) { synchronized (toacct) { new Helper().transfer(); } } } } } private class DollarAmount { } private abstract class Account { Comparable<DollarAmount> getBalance() { return new Comparable<DollarAmount>() { @Override public int compareTo(DollarAmount o) { // TODO Auto-generated method stub return 0; } }; } abstract void debit(DollarAmount a); abstract void credit(DollarAmount a); } }
在协作对象之间发生的死锁
以出租车调试系统为例
import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.image.ImageObserver; import java.awt.image.ImageProducer; import java.util.HashSet; import java.util.Set; class Taxi { private Point location, destination; private final Dispather dispather; public Taxi(Dispather dispather) { super(); this.dispather = dispather; } /** * @return the location */ public synchronized Point getLocation() { return location; } /** * @param location * the location to set */ public synchronized void setLocation(Point location) { this.location = location; if (location.equals(destination)) { dispather.notifyAvailable(this); } } } class Dispather { private final Set<Taxi> taxis; private final Set<Taxi> availableTaxis; public Dispather() { 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; } }
锁顺序很隐蔽:
其实最后还是简单的锁顺序死锁,对于synchronized可能比较熟悉的是synchronized(object){}的使用方法,对于synchronized方法不太熟悉,对于每个对象都有一个锁,可以这样理解synchronized(class test{ private int a,b..;});所以对象内的一个方法被调用的时候 ,这个对象的锁就被持有了,无论调用这个对象的哪个方法都需要等待正在被调用的方法执行完毕。
下面可以解释协作对象之间的死锁成因了:
Taxi调用setLocation的同时Dispather调用getImage。这个可以同时发生:
setLocation先获取taxi的锁,然后获取Dispather的锁(因为它调用了notifyAvailable方法)
同时
getImage先获取Dispather的锁,然后依次获取taxi的锁,而当获取到那个正在被调用setLocation的taxi时,死锁就发生了。
解决方法:采用类似客户端加锁的办法,把接口改为开放调用。
import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.image.ImageObserver; import java.awt.image.ImageProducer; import java.util.HashSet; import java.util.Set; class Taxi { private Point location, destination; private final Dispather dispather; public Taxi(Dispather dispather) { super(); this.dispather = dispather; } /** * @return the location */ public synchronized Point getLocation() { return location; } /** * @param location * the location to set */ public void setLocation(Point location) { boolean reached = false; synchronized (this) { this.location = location; reached = location.equals(destination); } if (reached) { dispather.notifyAvailable(this); } } } class Dispather { private final Set<Taxi> taxis; private final Set<Taxi> availableTaxis; public Dispather() { 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; } }
效果:
将两个锁减为了只竞争Dispather一个锁。Dispather从不持有taxi的锁。它调用的是taxis的副本。
因为getImage只用来获取数据,并不执行set类的方法。
此外收缩同步代码块的保护范围还可以提高可伸缩性。
死锁的避免与诊断
尽管是一个并发高手可能也会因为大意便程序发生死锁,而死锁的原因也很多,像上面那种因为协作对象之间而发生的死锁是非常难发现的。所以我们在写程序的过程中应该尽量避免锁的交互。同时应尽可能的使用开放调用。
支持定时的锁
这一项技术可以检测死锁并从死锁中恢复过来。即显式使用lock类的定时trylock功能来代替内置锁机制。显式锁可以设置一个超时时限。
在lock类里找到了这个接口 :
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
先了解一下,以后会细说
通过线程转储信息来分析死锁
JVM通过线程转储来帮助识别死锁的发生。
UNIX可以向jvm的进程发送SIGQUIT信号,或者在UNIX平台按下ctrl-\。windows平台按下ctrl+break(这个break键笔记本是没有的。只有87键及以上的键盘有。一般用来调试服务器。)
其它活跃性危险
饥饿
就是要执行任务的线程因为优先级等原因被迫等待,而造成的饥饿性等待。
你经常能发现某个程序会在一些奇怪的地方调用Thread.sleep和Thread.yield,这是因为该程序试图克服优先级调整问题或者响应性问题,并让低优先级的线程执行更多时间
要避免使用线程优先级,因为这会增加平台依赖性。并可能导致活跃性问题
糟糕的响应性
和饥饿是一类的问题,如果GUI应用程序中使用了后台线程,那么这种问题是很常见的。
活锁
活锁是另一种形式的活跃性问题,该问题尽管不用阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作。而且总会失败。
活锁通常发生在处理事务消息的应用程序中,如果不能成功的处理某个消息,那么消息处理机制将回滚整个事务。并将它重新放到队列的开头。所以程序不会死掉。但也不能继续执行了。
书上给了一个很形象的栗子,两个人走路遇见了,然后相互避让,结果又在另一条路上遇见,不断重复。
解决问题的办法是在重试机制中引入随机性。如,两台机器尝试使用相同的载波(就是频率,如果相同的话会发生混叠,使传输信号失真)来发送数据包,那么这些数据包就会发生冲突。并又双双重试。引入随机的概念后,让它们在等待随机的时间段后再重试。以太协议定义了重复发生冲突时采用指数方式回退机制,从而降低在多台存在冲突的机器之间发生拥塞和反复失败的风险。
在并发应用中,我们可以通过让程序等待随机长度的时间来避免活锁的发生。