之所以又開始研究IPC問題,是因爲昨天在51cto上閱讀學習了《多處理器編程的藝術(修訂版)》一書的第一章。第一章的內容確實讓我加深了多線程環境的印象,感覺很好,結果這一章最後的習題也就很自然的進入了我的任務列表。哲學家就餐問題是第一章習題的第一道題,博文內容屬於筆者思考的結果,如有轉載,請註明出處。
經典IPC問題:“哲學家就餐”
問題描述
習題1.
哲學家就餐問題是由併發處理的先驅E.W. Dijkstra所提出的,主要用於闡述死鎖和無飢餓概念。
假設五個哲學家一生只在思考和就餐。他們圍坐在一個大圓桌旁,桌上有一大盤米飯。然而只有五根可用的筷子。所有的哲學家都在思考。若某個哲學家餓了,則拿起自己身邊的兩根筷子。如果他能夠拿到這兩根筷子,則可以就餐。當這個哲學家吃完後,又放下筷子繼續思考。
解決方案要求
1. 試編寫模仿哲學家就餐行爲的程序,其中每個哲學家爲一個線程而筷子則是共享對象。注意,必須防止出現兩個哲學家同時使用同一根筷子的情形。
2. 修改所編寫的程序,不允許出現死鎖情形,也就是說,保證不會出現這樣的情形:每個哲學家都已擁有一根筷子,並且正在等待獲得另一個人手中的筷子。
3. 修改所編寫的程序使得不會出現飢餓現象。
4. 編寫能夠保證任意n個哲學家無飢餓就餐的程序。
問題分析與解決
首先根據問題描述,可以確定這樣的程序結構:每個哲學家是一個單獨的線程,而筷子數組則是這些線程的共享對象。
根據題目意思,爲所有哲學家設定相同的行動邏輯,如下:
①思考一段時間;
②拿起左筷;
③拿起右筷;
④吃飯;
⑤放下右筷;
⑥放下左筷;
⑦繼續思考,返回①。
如果爲每個哲學家配備了足夠的筷子,那麼這個問題就簡單了,只要啓動線程讓哲學家們行動就行了,它們之間不需要交互就能夠思考、吃飯。但這裏並沒有分配足夠的筷子給哲學家們用餐。因此問題的關鍵點在於,對一根筷子來說,需要保證在同一時刻只有一個哲學家在使用它。這其實就是多線程中的“互斥”概念。
多線程中的“互斥”,到了程序中的表現就是“互斥鎖”。“互斥”的意思就是,在稀缺資源(如單核計算機系統的CPU資源)的競爭中,僅能夠保證一個工作線程使用資源,其他工作線程需要在稀缺資源被釋放後纔有機會獲得稀缺資源的使用權。可以用生活中的例子做比喻,比如大城市上班高峯期的公交座位,春節前夕的火車上的洗手間等等。
帶入到這個問題去理解,筷子就是稀缺資源。更有趣的是,只有一根筷子是吃不了飯的。
既然在哲學家成功拿起一根筷子之後,不允許其他哲學家動這根筷子,那麼很簡單就要爲筷子加“互斥鎖“。Java中的內置鎖synchronized就是一種“互斥鎖”。
================解決“哲學家就餐“問題的第一種方案說明========================
使用“互斥鎖“管理每一根筷子。在哲學家”拿起“筷子之前,首先要獲得這根筷子的”互斥鎖“,這就表示持有了這根筷子。也就是說,首先獲取左筷的鎖,然後獲取右筷的鎖,接着就是成功吃到飯,然後放下右筷,放下左筷,繼續思考。
=============================================================================
下面是解決題目要求1的代碼(哲學家線程的工作代碼):
// 哲學家就餐邏輯 public void run() { int leftIndex = getLeftKzIndex(index); int rightIndex = getRightKzIndex(index); while (doWork) { // 嘗試拿起左筷 synchronized (kz[leftIndex]) { // 拿起左筷成功 // 嘗試拿起右筷 synchronized (kz[rightIndex]) { // 拿起右筷成功 // 成功吃飯 System.out.println("哲學家" + index + "成功吃到飯"); } } } }
核心部分就這麼點代碼,嗯,簡單,漂亮,精闢…可惜的是,點擊運行之後不一會就沒有了輸出,表示沒有哲學家吃到飯了。難道他們都吃飽了?開個玩笑。
這段代碼會導致“死鎖“發生。這個問題的死鎖狀態就是:5位哲學家都成功拿起了左筷,但都在等待獲取已經被別人拿去的另一根筷子。原因是沒有人會主動放棄筷子,所以全部”餓死“。
出現“死鎖“的原因找到了,狀態也分析了,那麼接下來的關鍵就是如何破壞掉”死鎖“狀態。思前想後,鄙人認爲只有兩種方法合適:第一種是允許哲學家查看其它哲學家的狀態,從而改變自己的行爲(比如哲學家拿起左手筷子之後,看到右邊那位仁兄拿了他的左手筷子,自己不知道他何時吃完,就主動放棄自己的左手筷子);第二種是採用超時的判斷,當嘗試獲取右筷(注意只是右筷)的”互斥鎖“超過一段時間,則主動放棄嘗試並捨棄已獲得的左筷。
這兩種方法的主要思想是,哲學家需要根據具體情況來放棄左筷,從而破壞“死鎖“條件。
分析一下第一種方法,發現用這種方法來實現會比較麻煩,而且會多出很多額外的線程安全問題。因爲如果要讓哲學家進程正確訪問到另一個哲學家線程的狀態,必須要有一組共享變量保存每個哲學家線程的狀態。與此同時,對這些狀態的讀寫都要做好併發控制,防止出現可見性問題。這些額外的工作會讓整個解決過程看起來很臃腫,不能夠很好的體現中心思想。另外,既然要將每個哲學家設爲一個線程,那麼相互之間的狀態最好不要相互告知爲好。
另一種通過判斷超時獲取鎖的方法就比較好,一方面Java併發庫有提供相應的實現,實現起來本身就不麻煩,同時代碼也不復雜,能夠較好地體現解決方法。
經過分析,筆者決定採用第二種方法。在代碼中需要使用到java.util.concurrent.locks.ReentrantLock類,它是一種相對於內置鎖synchronized更加靈活的鎖。不熟悉的可以參考API文檔,或者參考《Java Concurrency in Practice》(即《Java併發編程實戰》)第13章內容。
================解決“哲學家就餐“問題的第二種方案說明========================
使用“互斥鎖“管理每一根筷子。在哲學家”拿起“筷子之前,首先要獲得這根筷子的”互斥鎖“,這就表示持有了這根筷子。也就是說,首先獲取左筷的鎖,然後獲取右筷的鎖,接着就是成功吃到飯,然後放下右筷,放下左筷,繼續思考。
爲了破壞“死鎖”條件,在哲學家嘗試獲取右筷的鎖時,設定一個超時時間timeout。當嘗試獲取右筷的鎖的時間超過timeout,則主動放棄左筷,繼續思考。
=============================================================================
下面是能夠解決題目所有要求的代碼(哲學家線程的工作代碼):
public void run() { int leftIndex = getLeftKzIndex(index); int rightIndex = getRightKzIndex(index); while (doWork) { // 嘗試拿起左筷 synchronized (kz[leftIndex]) { // 拿起左筷成功 // 嘗試拿起右筷 try { // 每個筷子實例中都包含一個公有的ReentrantLock類實例lock if (!kz[rightIndex].lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) { // 拿起右筷失敗,繼續思考 continue; } // 拿起右筷成功 // 成功吃飯 System.out.println("哲學家" + index + "成功吃到飯"); } catch (InterruptedException e) { e.printStackTrace(); } finally { kz[rightIndex].lock.unlock(); } } } }
看到這裏,總算把這個問題給完美的解決了。上邊的方法是我獨立思考後認爲最好的方案。
但不是唯一的方案,下面我還要講一個最最簡單的方法。
================解決“哲學家就餐“問題的第三種方案說明========================
使用一個獨立的Object對象作爲唯一的“互斥“資源,哲學家每次進餐都只要獲得這個Object對象的”互斥鎖“,而不需要獲取兩個筷子的”互斥鎖“。也就是說,在同一時間只有一個哲學家可以進餐,也最多隻有兩根筷子被拿起。
=============================================================================
下面是解決方案三的代碼,同樣也能夠達到題目的所有要求(哲學家線程的工作代碼):
public void run() { int leftIndex = getLeftKzIndex(index); int rightIndex = getRightKzIndex(index); while (doWork) { System.out.println("哲學家" + index + "嘗試進餐..."); // lock對象聲明的位置沒有寫出來,只要認爲所有哲學家訪問到的是同一個Object lock就行了 synchronized (lock) { // 嘗試拿起左筷 // 拿起左筷成功 // 嘗試拿起右筷 // 拿起右筷成功 // 成功吃飯 System.out.println("哲學家" + index + "成功吃到飯"); } } }
這個方法的核心在於,保證了“哲學家就餐“這一複合操作的原子性。僅此而已。
…額,這個方法是不是太簡單了?覺得乾脆一開始就講出來就萬事大吉了吧。可是鄙人不認爲這種方法更好。具體的深入探討“哲學家就餐“問題見下一篇博客,敬請期待。