Java並發編程(十)死鎖
哲學家進餐問題
並發執行帶來的最棘手的問題莫過於死鎖了,死鎖問題中最經典的案例就是哲學家進餐問題:5個哲學家坐在一個桌子上,桌子上有5根筷子,每個哲學家的左手邊和右手邊各有一根筷子。示意圖如下:
哲學家進餐問題
並發執行帶來的最棘手的問題莫過於死鎖了,死鎖問題中最經典的案例就是哲學家進餐問題:5個哲學家坐在一個桌子上,桌子上有5根筷子,每個哲學家的左手邊和右手邊各有一根筷子。示意圖如下:
哲學家必須拿起左右兩邊的筷子才能進餐,如果他們同時拿起左手邊的筷子,就會導致死鎖。因為右手邊的筷子被他右邊的那位哲學家當成左手邊的筷子拿起來了,這樣一來這五位哲學家誰都沒有辦法進餐,他們死鎖了。
讓我們用代碼模擬這個死鎖:
class Philosopher implements Runnable { private int id; public Philosopher(int id) { this.id = id; } public void run() { int leftCsIndex = id; int rightCsIndex = (id+1)%5; synchronized(PhiloTest.chopsticks[leftCsIndex]) { System.out.println("I got left chopstick"); try { Thread.sleep(100); } catch (Exception e) {} synchronized(PhiloTest.chopsticks[rightCsIndex]) { System.out.println("I got right chopstick"); System.out.println("Philosopher"+ id+": eating"); } } } }public class PhiloTest { public static Object[] chopsticks = new Object[5]; public static void main(String[] args) { for(int i=0; i < chopsticks.length; i++) { chopsticks[i] = new Object(); } ExecutorService exec = Executors.newCachedThreadPool(); for(int i=0; i < 5; i++) { exec.execute(new Philosopher(i)); } exec.shutdown(); } }
輸出結果如下,並且程序始終沒有退出:
Philosopher0:I got left chopstick
Philosopher2:I got left chopstick
Philosopher1:I got left chopstick
Philosopher3:I got left chopstick
Philosopher4:I got left chopstick
我們創建了一個長度為5的數組,用來模擬筷子。此外我們定義了“哲學家線程”,每個哲學家都有自己的編號,我們假定哲學家左邊的筷子對應的是數組中索引和哲學家編號相同的對象,哲學家右邊的筷子對應的是數組中索引為哲學家編號加一的對象(註:第4個哲學家右手邊的筷子對應數組中第0個對象)。每個哲學家都先拿起左邊的筷子,為了保證所有的哲學家都拿到了左邊的筷子,每個哲學家拿到左邊的筷子後都等待100毫秒,然後再拿起右邊的筷子,這時他們死鎖了。
死鎖的條件
死鎖發生有四個條件,必須每個條件都滿足才有可能發生死鎖,只要破壞其中一個條件就不會死鎖。
1互斥:線程申請獲得的資源不能共享。在上面的例子中,每個哲學家不和別的哲學家共用一根筷子,反應在代碼上就是每個“哲學家線程”用鎖實現了互斥,一個哲學家拿到了對象的鎖,其它哲學家就不能拿到這個對象的鎖了。
2.持有並等待:線程在申請其它資源的時候不釋放已經持有的資源。在上面的例子中,哲學家在試圖去取右邊筷子的時候同時持有左邊的筷子。
3.不能搶占:線程持有的資源不能被其它線程搶占。在上面例子中,哲學家只能拿桌子上的筷子,不能從其它哲學家手裏搶筷子用。
4.循環等待:在上面的例子中,第0個哲學家在等待第1個哲學家放下筷子,第1個哲學家等第2個哲學家放下筷子....第4個哲學家等待第0個哲學家放下筷子,如此就形成了循環等待。
避免死鎖
避免死鎖最簡單的方法就是打破循環等待,比如5個哲學家中有一個哲學家先去拿右邊的筷子,再拿左邊的筷子,這樣就破壞了循環等待。實例代碼如下:
public class SolveDeadLock { public static Object[] chopsticks = new Object[5]; public static void main(String[] args) { for(int i=0; i < chopsticks.length; i++) { chopsticks[i] = new Object(); } ExecutorService exec = Executors.newCachedThreadPool(); for(int i=0; i < 4; i++) { exec.execute(new Philosopher(i)); } exec.shutdown(); int leftCsIndex = 4; int rightCsIndex = 0; synchronized(SolveDeadLock.chopsticks[rightCsIndex]) { System.out.println("Philosopher4:I got right chopstick"); try { Thread.sleep(100); } catch (Exception e) {} synchronized(SolveDeadLock.chopsticks[leftCsIndex]) { System.out.println("Philosopher4:I got left chopstick"); System.out.println("Philosopher4: eating"); } } } }
輸出結果:
Philosopher0:I got left chopstick
Philosopher2:I got left chopstick
Philosopher1:I got left chopstick
Philosopher3:I got left chopstick
Philosopher3:I got right chopstick
Philosopher3: eating
Philosopher2:I got right chopstick
Philosopher2: eating
Philosopher1:I got right chopstick
Philosopher1: eating
Philosopher0:I got right chopstick
Philosopher0: eating
Philosopher4:I got right chopstick
Philosopher4:I got left chopstick
Philosopher4: eating
上面的例子中我們修改了main()方法,使用主線程作為第4個哲學家,第四個哲學家先拿右面的筷子,再拿左面的筷子。這樣就避免了循環等待,因此這次沒有發生死鎖。在哲學家進餐案例中,互斥和持有並等待是不能規避的,因為這兩個是邏輯要求的,比如兩個哲學家同時使用一根筷子是違背常識的。因此除了第四個條件外,我們還可以通過搶占來規避死鎖。比如:設計一個“粗魯的哲學家”,這個哲學家如果沒有拿到筷子,就會去別的哲學家手裏面搶筷子,這樣就可以保證這個哲學家肯定可以吃到飯,一旦他放下筷子,就只有4個哲學家需要吃飯,而桌子上有5根筷子,這時肯定不會死鎖。由於篇幅原因,這裏就不使用代碼實現了,感興趣的讀者可以試著實現這個想法。
總結
在多線程系統中,許多概率性的問題是由於線程之間發生了死鎖,死鎖導致一些線程永遠都不會停止執行,雖然這些線程一直處於阻塞狀態,但是仍然占用內存空間,這樣就導致線程所占的內存空間永遠不會被釋放,這就是傳說中的內存泄露。線程死鎖是導致Java應用程序發生內存泄露的一個重要原因。因此在編寫代碼時一定要避免發生死鎖,避免死鎖最簡單的方法就是對資源進行排序,所有線程對資源的訪問都按照順序獲取,這樣就避免了循環等待,從而避免死鎖。
公眾號:今日說碼。關註我的公眾號,可查看連載文章。遇到不理解的問題,直接在公眾號留言即可。
哲學家必須拿起左右兩邊的筷子才能進餐,如果他們同時拿起左手邊的筷子,就會導致死鎖。因為右手邊的筷子被他右邊的那位哲學家當成左手邊的筷子拿起來了,這樣一來這五位哲學家誰都沒有辦法進餐,他們死鎖了。
讓我們用代碼模擬這個死鎖:
class Philosopher implements Runnable { private int id; public Philosopher(int id) { this.id = id; } public void run() { int leftCsIndex = id; int rightCsIndex = (id+1)%5; synchronized(PhiloTest.chopsticks[leftCsIndex]) { System.out.println("I got left chopstick"); try { Thread.sleep(100); } catch (Exception e) {} synchronized(PhiloTest.chopsticks[rightCsIndex]) { System.out.println("I got right chopstick"); System.out.println("Philosopher"+ id+": eating"); } } } } public class PhiloTest { public static Object[] chopsticks = new Object[5]; public static void main(String[] args) { for(int i=0; i < chopsticks.length; i++) { chopsticks[i] = new Object(); } ExecutorService exec = Executors.newCachedThreadPool(); for(int i=0; i < 5; i++) { exec.execute(new Philosopher(i)); } exec.shutdown(); } }
輸出結果如下,並且程序始終沒有退出:
Philosopher0:I got left chopstick
Philosopher2:I got left chopstick
Philosopher1:I got left chopstick
Philosopher3:I got left chopstick
Philosopher4:I got left chopstick
我們創建了一個長度為5的數組,用來模擬筷子。此外我們定義了“哲學家線程”,每個哲學家都有自己的編號,我們假定哲學家左邊的筷子對應的是數組中索引和哲學家編號相同的對象,哲學家右邊的筷子對應的是數組中索引為哲學家編號加一的對象(註:第4個哲學家右手邊的筷子對應數組中第0個對象)。每個哲學家都先拿起左邊的筷子,為了保證所有的哲學家都拿到了左邊的筷子,每個哲學家拿到左邊的筷子後都等待100毫秒,然後再拿起右邊的筷子,這時他們死鎖了。
死鎖的條件
死鎖發生有四個條件,必須每個條件都滿足才有可能發生死鎖,只要破壞其中一個條件就不會死鎖。
1互斥:線程申請獲得的資源不能共享。在上面的例子中,每個哲學家不和別的哲學家共用一根筷子,反應在代碼上就是每個“哲學家線程”用鎖實現了互斥,一個哲學家拿到了對象的鎖,其它哲學家就不能拿到這個對象的鎖了。
2.持有並等待:線程在申請其它資源的時候不釋放已經持有的資源。在上面的例子中,哲學家在試圖去取右邊筷子的時候同時持有左邊的筷子。
3.不能搶占:線程持有的資源不能被其它線程搶占。在上面例子中,哲學家只能拿桌子上的筷子,不能從其它哲學家手裏搶筷子用。
4.循環等待:在上面的例子中,第0個哲學家在等待第1個哲學家放下筷子,第1個哲學家等第2個哲學家放下筷子....第4個哲學家等待第0個哲學家放下筷子,如此就形成了循環等待。
避免死鎖
避免死鎖最簡單的方法就是打破循環等待,比如5個哲學家中有一個哲學家先去拿右邊的筷子,再拿左邊的筷子,這樣就破壞了循環等待。實例代碼如下:
public class SolveDeadLock { public static Object[] chopsticks = new Object[5]; public static void main(String[] args) { for(int i=0; i < chopsticks.length; i++) { chopsticks[i] = new Object(); } ExecutorService exec = Executors.newCachedThreadPool(); for(int i=0; i < 4; i++) { exec.execute(new Philosopher(i)); } exec.shutdown(); int leftCsIndex = 4; int rightCsIndex = 0; synchronized(SolveDeadLock.chopsticks[rightCsIndex]) { System.out.println("Philosopher4:I got right chopstick"); try { Thread.sleep(100); } catch (Exception e) {} synchronized(SolveDeadLock.chopsticks[leftCsIndex]) { System.out.println("Philosopher4:I got left chopstick"); System.out.println("Philosopher4: eating"); } } } }
輸出結果:
Philosopher0:I got left chopstick
Philosopher2:I got left chopstick
Philosopher1:I got left chopstick
Philosopher3:I got left chopstick
Philosopher3:I got right chopstick
Philosopher3: eating
Philosopher2:I got right chopstick
Philosopher2: eating
Philosopher1:I got right chopstick
Philosopher1: eating
Philosopher0:I got right chopstick
Philosopher0: eating
Philosopher4:I got right chopstick
Philosopher4:I got left chopstick
Philosopher4: eating
上面的例子中我們修改了main()方法,使用主線程作為第4個哲學家,第四個哲學家先拿右面的筷子,再拿左面的筷子。這樣就避免了循環等待,因此這次沒有發生死鎖。在哲學家進餐案例中,互斥和持有並等待是不能規避的,因為這兩個是邏輯要求的,比如兩個哲學家同時使用一根筷子是違背常識的。因此除了第四個條件外,我們還可以通過搶占來規避死鎖。比如:設計一個“粗魯的哲學家”,這個哲學家如果沒有拿到筷子,就會去別的哲學家手裏面搶筷子,這樣就可以保證這個哲學家肯定可以吃到飯,一旦他放下筷子,就只有4個哲學家需要吃飯,而桌子上有5根筷子,這時肯定不會死鎖。由於篇幅原因,這裏就不使用代碼實現了,感興趣的讀者可以試著實現這個想法。
總結
在多線程系統中,許多概率性的問題是由於線程之間發生了死鎖,死鎖導致一些線程永遠都不會停止執行,雖然這些線程一直處於阻塞狀態,但是仍然占用內存空間,這樣就導致線程所占的內存空間永遠不會被釋放,這就是傳說中的內存泄露。線程死鎖是導致Java應用程序發生內存泄露的一個重要原因。因此在編寫代碼時一定要避免發生死鎖,避免死鎖最簡單的方法就是對資源進行排序,所有線程對資源的訪問都按照順序獲取,這樣就避免了循環等待,從而避免死鎖。
公眾號:今日說碼。關註我的公眾號,可查看連載文章。遇到不理解的問題,直接在公眾號留言即可。
Java並發編程(十)死鎖