通过开放调用来避免死锁

一.概述

在编写并发程序的时候,我们经常会在一次业务逻辑处理过程中获取多个锁,然后做一些操作,如果这多个锁的获取顺序和其他地方的获取顺序不同的话,很容易发生死锁。为了避免这种死锁,我们必须保证获取多个锁的顺序一致,或者方法调用为开放调用,即调用某个方法时不需要持有锁。

二.示例分析

Taxi表示一个出租车对象,包含位置和目的地两个属性,Dispatcher代表一个出租车队。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Taxi {
	@GuardedBy("this")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(distination)) {
			dispatcher.notifyAvailable(this);
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Dispatcher {
	@GuardedBy("this") private final Set<Taxi> taxis;
	@GuardedBy("this") 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());
		}
		return image;
	}
}

看看上面的代码,我们就知道setLocation和notifyAvailable方法都是同步方法,调用setLocation的线程首先获取Taxi上的锁,然后获取Dispatcher的锁。getImage方法先获取Dispatcher上的锁,再获取Taxi上的锁,这两个方法被不同的线程调用时容易产生死锁,相信现在的滴滴以及快的绝对不是这么干的。

下面我看看如何通过开放调用来避免这个死锁的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Taxi {
	@GuardedBy("this")private Point location, 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 = false;
		/** 缩小缩的范围 把方法变成开发调用 **/
		synchronized(this) {
			this.location = location;
			reachedDestination = location.equals(distination);
		}
		
		if (reachedDestination) {
			dispatcher.notifyAvailable(this);
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Dispatcher {
	@GuardedBy("this") private final Set<Taxi> taxis;
	@GuardedBy("this") 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 : taxis) {
			image.drawMarker(t.getLocation());
		}
		return image;
	}
}

通过上面的改造,我们把setLocation和getImage都变成了开放调用,与那些持有锁时调用外部方法的程序相比,更容易对依赖的开发调用的程序进行死锁分析。

在编写并发程序的时候,一定要注意一个思想,加锁范围最小化。