1. 程式人生 > >高階程式設計師需知的併發程式設計知識(二)

高階程式設計師需知的併發程式設計知識(二)

### 說明 本篇是繼上一篇併發程式設計未討論完的內容的續篇。上一篇傳送門: [Java併發程式設計一萬字總結(吐血整理)](https://editor.csdn.net/md/?articleId=104642587) ## 活躍性問題 在上一篇我們討論併發程式設計帶來的風險的時候,說到其中 一個風險就是活躍性問題。活躍性問題其實就是我們的程式在某些場景或條件下執行不下去了。在這個話題下我們會去了解什麼是死鎖、活鎖以及飢餓,該如何避免這些情況的發生。 ### 死鎖 我們一般使用加鎖來保證執行緒安全,但是過度地使用加鎖,可能導致死鎖發生。 **哲學家進餐問題** “哲學家進餐”問題能很好地描述死鎖的場景。5個哲學家去吃火鍋,坐在一張圓桌上。它們有5根筷子(不是5雙),這5根筷子放在每個人的中間。哲學家時而思考,時而進餐。每個人都要取到一雙筷子才能吃到東西,並且在吃完後將筷子放回原處。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200322142424192.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTY1MDE4,size_16,color_FFFFFF,t_70) 可以考慮一下這種情況,如果每個人都立即抓住自己左邊的筷子,然後等待自己右邊的筷子空出來,但同時都不放手自己已經拿到的筷子。會出現什麼情況。可以想到,每個人都吃不上火鍋了,只等涼涼了。 **什麼是死鎖** 每個人都擁有其他人需要的資源,同時又等待其他人已經擁有的資源,並且每個人在獲得所需資源之前都不會放棄已經擁有的資源。這就是一種死鎖。 再使用執行緒的術語描述一下。線上程A持有鎖L並想獲得鎖M的同時,執行緒B持有鎖M並嘗試獲取鎖L,那麼這兩個執行緒將永遠地等待下去。 **簡單死鎖程式碼示例** ```java public class LeftRightDeadLock { private final Object left = new Object(); private final Object right = new Object(); public void leftRight(){ synchronized (left){ synchronized (right){ doSomething(); } } } public void rightLeft(){ synchronized (right){ synchronized (left){ doSomething(); } } } } ``` 上面的程式碼中,如果一個執行緒執行leftRight()方法,另一個執行緒呼叫rightLeft()方法,則會發生死鎖。 上面生產死鎖的原因是,兩個執行緒檢視以不同的順序來獲得相同的鎖。如果按照相同的順序請求鎖,那麼就不會出現迴圈的加鎖依賴性,因此就不會產生死鎖。 **產生死鎖的四個條件** 有個叫Coffman的牛人幫我們總結了產生死鎖的四個條件: 1. 互斥,共享資源X和Y只能被一個執行緒佔用 2. 佔用且等待,執行緒T1已經獲得了共享資源X,在等待共享資源Y的時候,不釋放共享資源X; 3. 不可搶佔,其他執行緒不能強行搶佔執行緒T1佔用的資源; 4. 迴圈等待,執行緒T1等待執行緒T2佔有的資源,執行緒T2等待執行緒T1佔有的資源,就是迴圈等待。 反過來說,我們只要破壞掉四個條件中的一個,就可以避免死鎖的發生。 首先第一個互斥條件沒法破壞,因為加鎖就是互斥的語義。 1. 對於“佔用且等待”的條件,我們可以一次性申請所有資源; 2. 對於“不可搶佔”這個條件,佔用部分資源的執行緒在申請其他資源時,如果申請不到,可以主動釋放它佔有的資源。 3. 對於“迴圈等待”這個條件,可以按照固定的順序申請資源,所有執行緒都按照規定的順序獲得鎖,這樣就不存在迴圈等待了。 ### 活鎖 活鎖是另一種形式的活躍性問題,該問題儘管不會阻塞執行緒,但也不能繼續執行下去,因為執行緒將不斷重複執行相同的操作,而且總會失敗。 當多個相互協作的執行緒都對彼此進行響應從而修改各自的狀態,並使得任何一個執行緒都無法繼續執行時,就發生了活鎖。就比如路上兩個人相遇,出於禮貌,都給對方讓路,結果每次都碰到一起。 要解決這種活鎖問題,需要在重試機制中加入隨機性。比如,在網路上,兩臺機器使用相同的載波來發送資料包,那麼這些資料包就會發生衝突。這兩臺機器都檢查到了衝突,並都在稍後再次重發。如果二者都選擇了在1秒後重試,那麼又會發生衝突,並且不斷地衝突下去,因而即使有大量閒置的頻寬,也無法將資料包傳送出去。**為避免這種情況的發生,需要讓他們分別等待一段隨機的時間,這樣就能避免活鎖的發生了。** ### 飢餓 **“飢餓”就是當執行緒由於無法訪問它所需要的資源而不能繼續執行時的場景。**所謂“不患寡而患不均”。當某些執行緒一直獲取不到CPU執行資源的時候,就發生了“飢餓”。 一些容易導致飢餓的場景: 1. 在應用中對Java執行緒優先順序的使用不當。(因為JVM會將Thread API中的10個優先順序對映到作業系統的排程優先順序上,這就可能存在兩個不同的優先順序被對映到了作業系統層的同一個優先順序,因此儘量不要改變執行緒優先順序) 2. 持有鎖的執行緒,如果執行的時間過程或者存在無限迴圈,也可能導致“飢餓”問題。 解決“飢餓”問題的一般方案就是使用公平鎖(注意synchronized術語非公平鎖)。 ## JUC工具類庫 Java併發包給我們提供了非常豐富的構建併發程式的基礎模組,例如執行緒安全容器類、同步工具類,阻塞佇列等。 **這些工具類都在java.util.concurrent包下面,所以簡稱J.U.C工具包。** ### 同步容器和併發容器 **同步容器類有哪些** 主要有Vector和Hashtable,這兩個都是早期JDK的一部分,還有一些封裝器類是由Collections.sychronizedXxx等工廠方法建立的。 這些類實現執行緒安全的方式都是:將他們的狀態封裝起來,並對每個公有方法都進行同步,也就是使用synchronized內建鎖的方式,使得每次只有一個執行緒能訪問容器的狀態。 **同步容器類的問題** 同步容器類雖然是執行緒安全的類,但是在某些場景下可能需要額外的客戶端加鎖來保護複合操作的執行緒安全性。比如迭代(反覆訪問元素,直到遍歷完容器中的所有元素)、條件運算(如果沒有則新增)。下面給出一個示例說明下: ```java public class GetLastElement implements Runnable{ priv