死鎖
- 前言
- 可重入鎖
- 邏輯
- 兩個線程兩把鎖(死鎖)
- 死鎖的特點
- 多個線程多把鎖(哲學家就餐問題)
- 總結
前言
在前面的文章中,介紹了鎖的基本使用方式——鎖
在上一篇文章中,通過synchronized關鍵字進行加鎖操作,使得【多線程修改同一變量】的情況可以得到解決。
那么在本文中,將會繼續講解鎖的相關知識點。
可重入鎖
我們可以通過synchronized確定鎖對象,對線程進行加鎖的操作。那么如果鎖對象重復使用是否會出現不一樣的結果?
在下面的案例中,t1和t2線程使用了連續synchronized,設置的加鎖對象同樣是counter,如果運行這段代碼,結果卻是正確的。
理論上,當synchronized(counter)開始使用時,只有執行完其中的代碼(大括號中的代碼塊)才會釋放鎖。而這個鎖中又嵌套了相同的鎖,按道理來說此時counter鎖對象還沒有被釋放,應該出現阻塞等待狀態最終代碼無法運行才是。
那么為什么在Java中這段代碼可以編譯通過?
原因: 在Java中,已經對synchronized內部進行了特殊處理。在每個鎖對象中,會記錄當前是哪個線程持有這把鎖,接下來,當針對這個對象進行加鎖操作的時候就會進行判定,判定當前嘗試加鎖線程是否是這一對象鎖的線程。 通過這樣的操作,系統就可以知道如果不是,就會阻塞;如果是,就會直接放行。
class Counter {public static int count;void add(){count++;}public int getCount(){return count;}
}public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {synchronized (counter) {synchronized (counter) {counter.add();}}}});Thread t2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {synchronized (counter) {synchronized (counter) {counter.add();}}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+counter.getCount());}
邏輯
當加了多層鎖的時候,代碼如何知道執行到哪里要真正進行解鎖。如果有若干層加鎖操作,如何判定當前遇到的}是最外層的} ?
以我的理解,進行加鎖操作時在內部給鎖對象設計了一個計數器(int n)。 每次遇到【{ 】時,n++,遇到【 } 】時,n–,當n=0時才真正解鎖。
通過這樣的方式,針對同一線程中的同一鎖對象進行的鎖操作,可以讓程序猿避免了死鎖的情況,我們也稱之為可重入鎖。
兩個線程兩把鎖(死鎖)
現在存在線程t1和線程t2;鎖對象A和B。
當進行鎖操作的時候,可能會出現這樣的情況:線程1和線程2都需要使用到鎖A和鎖B,對于線程A來說,首先獲取鎖A然后獲取鎖B;而對于B來說,首先獲取鎖B再獲取鎖A。
讓兩個線程同時獲得第一把鎖,接下來需要嘗試去獲取對方的鎖。
在下面的代碼啟動后,線程t1獲取到了鎖locker1,線程t2獲取到了鎖locker2.
對于t1,接下來需要獲取locker2才能繼續執行接下來的代碼
對于t2,需要獲取locker1才能繼續執行接下來的代碼
兩個線程之間無法讓步,于是一同進入了阻塞等待狀態,都在等待對方釋放鎖。
public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()-> {synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("獲取到了2把鎖");}}});Thread t2 = new Thread(()-> {synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("獲取到了2把鎖");}}});t1.start();t2.start();}
在執行這段代碼后,我們可以通過jconsole查看線程狀態。如下圖所示,我們可以知道兩個線程都處于阻塞狀態,同時我們也可以知道阻塞所需要獲取的鎖目前在哪個線程身上。
死鎖的特點
1.鎖具有互斥性。這是鎖的基本特點,當一個線程拿到鎖A時,其他線程只能等待該線程釋放。
2.鎖不可搶占。只有該線程主動釋放鎖,別的線程無法搶占。
3.請求和保持。線程拿到鎖以后可以繼續嘗試獲取其他鎖。
4.循環等待。多個線程多個鎖的狀態中,出現了A等待B,B等待A的情況。
當全部滿足這些條件以后,就可以發生死鎖。
多個線程多把鎖(哲學家就餐問題)
情景:在一個餐桌上存在五個哲學家,他們做兩件事情:一是思考哲學,二是就餐。
每個哲學家左右手各有一根筷子以供就餐時使用。如果哲學家手中只有一根筷子,他會等到另一根筷子擁有的時候才會就餐,而不會放下筷子。
通過這個情景,我們可以很明顯的預知到一種情況:所有的哲學家手中都拿起左邊的筷子,于是所有哲學家都停下了,進入阻塞等待狀態。
我們通過這個問題可以反映到線程的情況,因為多線程多鎖的原因,如果沒有合理安排則會導致線程阻塞甚至死鎖!
解決這種情況的一種辦法就是約定好加鎖的順序,破除循環等待的情況。
在下面的代碼中,兩個線程輪流獲取locker1和locker2,這就可以有效規避死鎖的問題了。
public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()-> {synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("獲取到了2把鎖");}}});Thread t2 = new Thread(()-> {synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("獲取到了2把鎖");}}});t1.start();t2.start();}
總結
死鎖在多線程中是及其常見的一個問題,導致了線程的不安全。是我們要極力規避的情況。我們了解了死鎖發生的情況,死鎖的原因等多個點。
本文使用源碼? 源碼