百川东到海,何日复西归?少壮不努力,老大徒伤悲。———《汉乐府·长歌行》
河水东流不复返就如时间的流逝不可能停留,告诫我们年少时如果不珍惜时间努力向上,到老只能白白地悔恨自己与悲伤了。上文讲解了锁顺序死锁发生的情况,但是还存在一些死锁的情况,不是顺序锁那么简单可以被发现。因为有可能并不是在同一个方法中显示请求两个锁,而是嵌套另一个方法去获取第二个锁。
我们举个例子来说明,下面代码清单姑且当做是表示一个出租车系统吧,Taxi 表示出租车,具有位置和方向两个属性;Dispatcher表示一组出租车。
import java.awt.Point;import java.util.HashSet;import java.util.Set;public class ThreadDeadLockTest{class Taxi{private Point location, destation;private final Dispatcher dispatcher;public Taxi(Dispatcher dispatcher) {this.dispatcher = dispatcher; }//获取同步锁public synchronized Point getLocation() {return location; }//获取Taxi同步锁,同时嵌套的方法还有获取Dispatcher的同步锁public synchronized void setLocation(Point location) {this.location = location;if(location.equals(destation)) {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);}//获取同步锁,同时还会获取Taxi的同步锁public synchronized Image getImage() {Image image = new Image();for(Taxi t : taxis) {image.drawer(t.getLocation());} return image;}}}
从上面代码可以看出,setLocation 和 getImage 方法都会获取两个锁。假如一个线程调用了 setLocation 方法,那么首先会获取到一个 Taxi 的锁,然后嵌套调用了 notifyAvailable,或获取到 Dispatcher 的锁。同样,另外一个线程调用了 getImage 方法,首先他会获取 Dispatcher 的锁,然后获取 Taxi 的锁。所以两个线程会以不同的顺序占用锁,那么就会产生死锁的情况。
线程A --> Taxi的锁 --> Dispatcher 的锁线程B --> Dispatcher的锁 --> Taxi 的锁
- 1
- 2
对于上面这种会发生死锁的情况,往往是比较难察觉的。这告诉我们,在持有锁的时候,调用外部方法可能会获取其他锁或者遭遇严重的超时阻塞,由于你不知道外部方法的情况,这通常很难进行分析。
当调用的方法不需要持有锁时,这被称为开放调用,它有着更好的行为。如何理解这句话?让我们来重构上面的代码,使其变成开放调用,从而避免死锁问题。我们需要减少 synchronize 的使用,只守护那些调用共享状态的操作。如下代码清单所示:
import java.awt.Point;import java.util.HashSet;import java.util.Set;public class ThreadDeadLockTest{class Taxi{private Point location, destation;private final Dispatcher dispatcher;public Taxi(Dispatcher dispatcher) {this.dispatcher = dispatcher;}//获取同步锁public synchronized Point getLocation() {return location;}//缩小锁的范围,不会同时去获取两个锁public void setLocation(Point location) {boolean isReached = false;//缩小锁的范围synchronized (this) {this.location = location;isReached = location.equals(destation); }if(isReached){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 Image getImage() {Set<Taxi> copy;//缩小锁的范围synchronized(this) {copy = new HashSet<Taxi>(taxis);}Image image = new Image();for(Taxi t : copy) {image.drawer(t.getLocation());}return image;}}}
getImage 和 setLocation 都缩小了锁的范围,获取第二锁之前已经释放了第一个锁,所以不会产生顺序死锁问题了。但是可能的情况是,可能会使得一个原子操作变为非原子操作。有时候这样的损失是可以接受的。比如这里 copy 的时候,可能不是瞬时最新的值。
如果一个程序一次之多获取一个锁,那么就不会产生锁顺序死锁。所以,尽量减少潜在锁之间的交互数量,遵守并文档化该锁顺序协议,缺一不可。
本文完结。
| 欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) | 黑马程序员IT技术论坛 X3.2 |