java併發編程(九): 避免活躍性危險

避免活躍性危險:

  • 本部分討論活躍性故障的緣由,及如何避免它們。

死鎖:

  • 典型的哲學家進餐問題。

鎖順序死鎖:

如上面哲學家進餐有可能發生下面的狀況: java

  • 上面發生死鎖的根本緣由在於兩個線程以不一樣的順序來獲取相同的鎖
  • 若是全部線程都以固定的順序獲取鎖,那麼程序就不會出現鎖順序死鎖問題。
/**
 * 容易由於獲取鎖的順序致使死鎖
 */
public class LeftRightDeadLock {
	private final Object left = new Object();
	private final Object right = new Object();
	
	public void leftRight(){
		synchronized(left){
			synchronized(right){
				// to do sth.
			}
		}
	}
	
	public void rightLeft(){
		synchronized(right){
			synchronized(left){
				// to do sth.
			}
		}
	}
}

動態的鎖順序死鎖:

  • 典型的就是銀行轉帳問題
public void transferMoney(Account fromAccount, Account toAccount, int money){
	synchronized (fromAccount) {
		synchronized(toAccount){
			if (fromAccount.getBalance() > money){
				//餘額不足
			} else{
				fromAccount.debit(money);
				toAccount.credit(money);
			}
		}
	}
}
當咱們如下面這種方式調用,就有可能出現死鎖:
transferMoney(a1, a2, money);
transferMoney(a2, a1, money);
要解決這種問題,就得使內部以相同的順序加鎖,不管外部怎麼調用。
/**
 * 用於當輸入參數的hash值同樣時使用
 */
private static final Object tieLock = new Object();
	
public static void transferMoney(final Account fromAccount, 
		final Account toAccount, final int money){
	class Helper{
		public void transfer(){
			if (fromAccount.getBalance() < money){
				//餘額不足
			} else{
				fromAccount.debit(money);
				toAccount.credit(money);
			}
		}
	}
		
	int fromHash = System.identityHashCode(fromAccount);
	int toHash = System.identityHashCode(toAccount);
		
	//不管客戶端怎麼傳入參數,咱們都以先鎖定hash值小的,再鎖定hash大的
	//也能夠利用業務中排序關係,如Account的編號等來比較
	if (fromHash < toHash){
		synchronized (fromAccount){
			synchronized (toAccount) {
				new Helper().transfer();
			}
		}
	} else if (fromHash >  toHash){
		synchronized (toAccount){
			synchronized (fromAccount) {
				new Helper().transfer();
			}
		}
	} else { //hash值相等, 狀況很小
		synchronized (tieLock) {
			synchronized (fromAccount) {
				synchronized (toAccount) {
					new Helper().transfer();
				}
			}
		}
	}
}
在協做對象之間發生的死鎖:
  • 即在不一樣方法中相互持有等待得鎖。
class Taxi {
	private Point location; 
	private Point 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.notifyAvaliable(this);
		}
	}
}
	
class Dispatcher {
	private final Set<Taxi> taxis;
	private final Set<Taxi> avaliableTaxis;
		
	public Dispatcher(){
		taxis = new HashSet<>();
		avaliableTaxis = new HashSet<>();
	}

	public synchronized void notifyAvaliable(Taxi taxi) {
		avaliableTaxis.add(taxi);
	}
		
	public synchronized Image getImage(){
		Image image = new Image();
		for (Taxi t :taxis){
			image.drawMarker(t.getLocation());
		}
		return image;
	}
}
上面的 setLocationgetImage就有可能發生死鎖現象:setLocation獲取到Taxi對象鎖後,在dispacher.notifiyAvaliable()時須要dispatcher鎖,而getImage獲取到dispacher鎖後,t.getLocation要求Taxi鎖。
  • 若是在持有鎖時調用某個外部方法,那麼將出現活躍性問題,在這個外部方法中可能會獲取其餘鎖(這可能會產生死鎖),或者阻塞時間過長,致使其餘線程沒法及時得到當前被持有的鎖。

開放調用:

  • 若是在調用某個方法時不須要持有鎖,那麼這種調用就被稱爲開放調用(Open Call)
/**
 * 經過公開調用來避免在相互協做的對象之間產生死鎖
 */
public class OpenCall {
	class Taxi {
		private Point location; 
		private Point 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;
			synchronized(this){
				this.location = location;
				reachedDestination = location.equals(destination);
			}
			if (reachedDestination){
				dispatcher.notifyAvaliable(this); //這裏持有dispatcher鎖,但已釋放taxi鎖
			}
		}
	}
	
	class Dispatcher {
		private final Set<Taxi> taxis;
		private final Set<Taxi> avaliableTaxis;
		
		public Dispatcher(){
			taxis = new HashSet<>();
			avaliableTaxis = new HashSet<>();
		}

		public synchronized void notifyAvaliable(Taxi taxi) {
			avaliableTaxis.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;
		}
	}
}
  • 在程序中儘可能使用開放調用。與那些在持有鎖調用外部方法的程序時,更易於對依賴於開放調用的程序進行死鎖分析。

資源死鎖:

  • 多個資源池(如數據庫鏈接池),一個線程須要鏈接2個數據庫鏈接,如線程A持有Pool1的鏈接,等待Pool2的鏈接;線程B持有Pool2的鏈接,等待Pool1的鏈接。
  • 程飢餓死鎖,如一個任務中提交另外一個任務,並一直等待被提交任務完成。
  • 有界線程池/資源池與相互依賴的任務不能一塊兒使用。

死鎖的避免與診斷:

支持定時的鎖:

  • 限時等待。如Lock中的tryLock, 給定一個超時時限,若等待超過該時間,則會給出錯誤信息,避免永久等待。

經過線程轉儲信息來分析死鎖:

其餘活躍性危險:

飢餓:

  • 因爲線程沒法訪問它所需的資源而不能繼續執行時,就發生了"飢餓"。
  • 避免使用線程優先級,這會增長平臺依賴性,並可能致使活躍性問題,在大多數併發應用程序中,均可以使用默認的線程優先級。

糟糕的響應性:

  • 例如GUI程序中使用了後臺線程。該後臺任務若爲CPU密集型,將可能影響程序響應性。
  • 不良的鎖管理也可能致使糟糕的響應性。

活鎖:

  • 活鎖是另外一種形式的活躍性問題,該問題儘管不會阻塞線程,但也不能繼續執行,由於線程將不斷重複執行相同的操做,並且總會失敗
  • 多個相互協做的線程都對彼此進行響應從而修改各自的狀態,並使得任何一個線程都沒法繼續執行時,就發生了活鎖。
  • 在併發應用中,經過等待隨機長度的時間和回退能夠有效的避免活鎖的發生。

不吝指正。 數據庫

相關文章
相關標籤/搜索