java編程思想之併發(死鎖)

一個對象能夠有 synchronized 方法或其餘形式的加鎖機制來防止別的任務在互斥尚未釋放的時候就訪問這個對象。java

死鎖

任務有可能變成阻塞狀態,因此就可能發生這樣的狀況:某個任務在等待另外一個任務,然後者又在等待別的任務,這樣一直下去,直到這個鏈條上的任務又在等待第一個任務釋放鎖。這就造成了一個相互等待的循環,沒有那個線程可以繼續。這被稱之爲死鎖。編程

咱們真正須要解決的問題是程序看起來可能工做良好,可是具備潛在的死鎖風險。這時,死鎖可能發生,而事先卻沒有任何徵兆,因此缺陷會潛伏在你的程序裏,直到被人之外的發現了。所以,在編寫併發編程的時候,進行仔細的程序設計以防止死鎖是很是關鍵的。bash

下面引入一個問題,一共有 5 個哲學家。這些哲學家將花部分時間思考,花部分時間就餐。做爲哲學家他們很窮,因此他們只能買 5 根筷子。他們圍坐在桌子的周圍,每人之間放一根筷子。當一個哲學家要就餐的時候,這個哲學家必須同時獲得左邊和右邊的筷子。若是一個哲學家左邊或者右邊已經獲得筷子,那麼這個哲學家就必須等待,直至可獲得必須的筷子。併發

public class Chopstick {
	private boolean taken = false;

	public synchronized void take() throws InterruptedException{
		while (taken) {
			wait();
		}
		taken = true;
	}

	public synchronized void drop() {
		taken = false;
		notifyAll();
	}
}

複製代碼

任何兩個哲學家都不能使用同一根筷子。也就是不能同時 taken() 同一個筷子。另外若是一個 Chopstick 被一個哲學家得到,那麼另外一個哲學家能夠 wait(),直到當前的這根筷子的持有者調用 drop() 結束使用。dom

public class Philosopher implements Runnable{

	private Chopstick left;
	private Chopstick right;
	private final int id;
	private final int ponderFactor;
	private Random random = new Random(47);
	public void pause() throws InterruptedException{
		if (ponderFactor ==0) {
			return;
		}

		TimeUnit.MICROSECONDS.sleep(random.nextInt(ponderFactor * 250));
	}
	protected Philosopher(Chopstick left, Chopstick right, int id, int ponderFactor) {
		super();
		this.left = left;
		this.right = right;
		this.id = id;
		this.ponderFactor = ponderFactor;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		try {
			while (!Thread.interrupted()) {
				System.out.println(this+"開始思考");
				pause();
				System.out.println(this+"開始拿左邊的筷子");
				left.take();
				System.out.println(this+"開始拿右邊的筷子");
				right.take();

				System.out.println(this+"開始就餐");
				pause();
				left.drop();
				right.drop();
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			System.out.println("當前線程被中斷了");
		}
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "哲學家的編號:"+id;
	}
}

複製代碼

在哲學家的任務中,每一個哲學家都是不斷的思考和吃飯。若是 ponderFactor 不爲 0,則 pause() 就會休眠一會。經過這樣的方法你會看到哲學家會思考一段時間。而後嘗試着去獲取左邊和右邊的筷子,隨後再在吃飯上花掉一段隨機的時間,以後重複此過程。ide

如今咱們來創建這個程序的死鎖版本:ui

public class DeadLockingDiningPhilosophers {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int ponder = 5;
		int size = 5;
		ExecutorService service = Executors.newCachedThreadPool();
		Chopstick[] sChopsticks = new Chopstick[size];
		for (int i = 0; i < size; i++) {
			sChopsticks[i] = new Chopstick();
		}
		for (int i = 0; i < size; i++) {
			//每個哲學家都會持有他左邊和右邊的筷子對象
			service.execute(new Philosopher(sChopsticks[i],sChopsticks[(i+1)%size],i,ponder));
		}
		System.out.println("執行結束");
		service.shutdownNow();
	}

}
複製代碼

執行的結果:this

執行結束
哲學家的編號:2開始思考
哲學家的編號:4開始思考
哲學家的編號:1開始思考
哲學家的編號:0開始思考
哲學家的編號:3開始思考
當前線程被中斷了
當前線程被中斷了
當前線程被中斷了
當前線程被中斷了
當前線程被中斷了
複製代碼

這個程序表示每個哲學家都有可能要表示進餐,從而等待其臨近的 Philosopher 放下他們的 Chopstick。這將會使得程序死鎖。spa

要修正死鎖必須明白,當如下四個條件同時知足時,就會發生死鎖:線程

  1. 互斥條件。任務使用的資源中至少有一個是不能共享的。
  2. 至少有一個任務必須持有一個資源,而且正在等待獲取一個當前被別的任務持有的資源。也就是說必須是拿着一根筷子等待另外一個筷子。
  3. 資源不能被任務搶佔,任務必須把資源釋放當作普通事件。你不能搶別人手裏的筷子。
  4. 必須有循環等待,這時一個任務等待其餘任務所持有的資源,後者又在等待另外一個任務所持有的資源,這樣循環下去直到有一個任務等待第一個任務所持有的資源,使得你們都被鎖住。

由於要發生死鎖全部這些條件必須知足;因此要防止死鎖的話只須要破壞其中一個就能夠。在程序中防止死鎖的最容易的辦法就是破壞第四個循環條件。

public class FixedDiningPhilosophers {
	 public static void main(String[] args) throws Exception {
		    int ponder = 5;
		    if(args.length > 0)
		      ponder = Integer.parseInt(args[0]);
		    int size = 5;
		    if(args.length > 1)
		      size = Integer.parseInt(args[1]);
		    ExecutorService exec = Executors.newCachedThreadPool();
		    Chopstick[] sticks = new Chopstick[size];
		    for(int i = 0; i < size; i++)
		      sticks[i] = new Chopstick();
		    for(int i = 0; i < size; i++)
		      if(i < (size-1))
		        exec.execute(new Philosopher(sticks[i], sticks[i+1], i, ponder));
		      else
		        exec.execute(new Philosopher(sticks[0], sticks[i], i, ponder));
		    if(args.length == 3 && args[2].equals("timeout"))
		      TimeUnit.SECONDS.sleep(5);
		    else {
		      System.out.println("Press 'Enter' to quit");
		      System.in.read();
		    }
		    exec.shutdownNow();
		  }
}

複製代碼

執行結果:

哲學家的編號:2開始拿左邊的筷子
哲學家的編號:4開始思考
哲學家的編號:1開始思考
哲學家的編號:0開始拿左邊的筷子
哲學家的編號:0開始拿右邊的筷子
哲學家的編號:0開始就餐
哲學家的編號:3開始就餐
哲學家的編號:2開始拿右邊的筷子
/.....
複製代碼

經過確保最後一個哲學家先拿起和放下左邊的筷子,咱們能夠移除死鎖,從而使得程序運行。Java 並無對死鎖提供類庫上的支持;可否經過仔細的程序設計避免死鎖須要咱們本身努力。

個人博客

掃碼關注公衆號便可當即獲取全套的 Java 編程思想讀書筆記。

相關文章
相關標籤/搜索