當(dāng)前位置:首頁 > 公眾號精選 > 程序員小灰
[導(dǎo)讀]死鎖的現(xiàn)象想象一個場景,賬戶A給賬戶B轉(zhuǎn)賬,同時賬戶B也給賬戶A轉(zhuǎn)賬,兩個賬戶都需要鎖住余額,所以通常會申請兩把鎖,轉(zhuǎn)賬時,先鎖住自己的賬戶,并獲取對方的鎖,保證同一時刻只能有一個線程去執(zhí)行轉(zhuǎn)賬。這時可能就會出現(xiàn),對方給我轉(zhuǎn)賬,同時我也給對方轉(zhuǎn)賬,那么雙方都持有自己的鎖,且嘗試去...


死鎖的現(xiàn)象


想象一個場景,賬戶A給賬戶B轉(zhuǎn)賬,同時賬戶B也給賬戶A轉(zhuǎn)賬,兩個賬戶都需要鎖住余額,所以通常會申請兩把鎖,轉(zhuǎn)賬時,先鎖住自己的賬戶,并獲取對方的鎖,保證同一時刻只能有一個線程去執(zhí)行轉(zhuǎn)賬。


這時可能就會出現(xiàn),對方給我轉(zhuǎn)賬,同時我也給對方轉(zhuǎn)賬,那么雙方都持有自己的鎖,且嘗試去獲取對方的鎖,這就造成可能一直申請不到對方的鎖,循環(huán)等待,就會發(fā)生“死鎖”。


一旦發(fā)生死鎖,線程一直占用著資源無法釋放,又無法完成轉(zhuǎn)賬,就會造成系統(tǒng)假死。


什么是死鎖?


“死鎖”就是兩個或兩個以上的線程在執(zhí)行過程中,互相持有對方所需要的資源,導(dǎo)致這些線程處于等待狀態(tài),無法繼續(xù)執(zhí)行。若無外力作用,它們都將無法繼續(xù)執(zhí)行下去,就進(jìn)入了“永久”阻塞的狀態(tài)。

圖1 死鎖的現(xiàn)象


如圖所示,線程1獲取了資源1,同時去請求獲取資源2,但是線程2已經(jīng)占有資源2了,所以線程1只能等待。同樣的,線程2占有了資源2,要請求獲取資源1,但資源1已經(jīng)被線程1占有了,只能等待。于是線程1和線程2都在等待持有對方的持有的資源,就會無限等待下去,這就是死鎖現(xiàn)象。


模擬發(fā)生死鎖的場景


下面寫一段代碼,模擬兩個線程各自持有了鎖,然后請求獲取對方持有的鎖,發(fā)生死鎖的現(xiàn)象。

public class DeadLock { public static String obj1 = "obj1"; public static String obj2 = "obj2";
public static void main(String[] args) { Thread a = new Thread(new Lock1()); Thread b = new Thread(new Lock2()); a.start(); b.start(); }
static class Lock1 implements Runnable { @Override public void run() { try { System.out.println("Lock1 running"); synchronized (DeadLock.obj1) { System.out.println("Lock1 lock obj1"); Thread.sleep(5000); synchronized (DeadLock.obj2) { System.out.println("Lock1 lock obj2"); } } } catch (Exception e) { e.printStackTrace(); } } }
static class Lock2 implements Runnable { @Override public void run() { try { System.out.println("Lock2 running"); synchronized (DeadLock.obj2) { System.out.println("Lock2 lock obj2"); Thread.sleep(5000); synchronized (DeadLock.obj1) { System.out.println("Lock2 lock obj1"); } } } catch (Exception e) { e.printStackTrace(); } } }}程序啟動后,從控制臺輸出,就能看出兩個線程都沒有結(jié)束,而是被卡住了。

圖2?死鎖demo輸出


我們用jvisualVM看下線程的堆棧信息:


圖3?jvisualVM堆棧信息


我們用jvisualVM查看線程的堆棧信息,發(fā)現(xiàn)已經(jīng)檢測到了死鎖的存在,而且定位到了具體的代碼行。


死鎖產(chǎn)生的原因


死鎖的發(fā)生也必須具備一定的條件,必須具備以下四個條件:


  • 互斥,共享資源 X 和 Y 只能被一個線程占用;
  • 占有且等待,線程01 已經(jīng)取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
  • 不可搶占,其他線程不能強(qiáng)行搶占線程01 占有的資源;
  • 循環(huán)等待,線程01 等待線程02 占有的資源,線程02 等待線程01 占有的資源,就是循環(huán)等待。

如何避免死鎖?


死鎖一旦發(fā)生,并沒有什么好的方法解決,通常我們只能避免死鎖的發(fā)生。

怎么避免呢?那就要看針對死鎖發(fā)生的原因去解決。

  1. 首先,“互斥”是沒有辦法避免的,你想從賬戶A轉(zhuǎn)賬到賬戶B,就必須加鎖,就沒法避免互斥的存在。
  2. 對于“占用且等待”這個條件,我們可以一次性申請所有的資源,這樣就不存在等待了。
  3. 對于“不可搶占”這個條件,占用部分資源的線程進(jìn)一步申請其他資源時,如果申請不到,可以在一定時間后,主動釋放它占有的資源,這樣就解決了不可搶占這個條件。
  4. 對于“循環(huán)等待”,我們可以靠按“次序”申請資源來預(yù)防。所謂按序申請,就是給資源設(shè)定順序,申請的時候可以先申請序號小的資源,再申請序號大的,這樣資源線性化后,自然就不存在循環(huán)等待了。

所以,總結(jié)來看,避免死鎖的發(fā)生有三種方法:破壞占用且等待的條件、破壞不可搶占條件、破壞循環(huán)等待條件。

1、破壞占用且等待條件
我們要破壞占用且等待,就是一次性申請占有所有的資源。賬戶A給賬戶B轉(zhuǎn)賬,就可以一次性申請賬戶A和賬戶B的鎖,同時拿到兩個鎖之后,在執(zhí)行轉(zhuǎn)賬操作。
public?class?DeadLock2?{ public static void main(String[] args) { Account a = new Account(); Account b = new Account(); a.transfer(b, 100); b.transfer(a, 200); }
static class Allocator { private List als = new ArrayList<>();
private void Allocator() { }
synchronized boolean apply(Account from, Account to) { if (als.contains(from) || als.contains(to)) { return false; } else { als.add(from); als.add(to); } return true; }
synchronized void clean(Account from, Account to) { als.remove(from); als.remove(to); } }
static class Account { private Allocator actr = DeadLock2.getInstance(); private int balance;
void transfer(Account target, int amt) { while (!actr.apply(this, target)){ } try { synchronized (this) { System.out.println(this.toString() " lock lock1"); synchronized (target) { System.out.println(this.toString() " lock lock2"); if (this.balance > amt) { this.balance -= amt; target.balance = amt; } } } } finally { actr.clean(this, target); } } }
private static class SingleTonHoler { private static Allocator INSTANCE = new Allocator(); }
public static Allocator getInstance() { return SingleTonHoler.INSTANCE; }}輸出結(jié)果如下:

圖4?破壞占用且等待條件輸出


從輸出結(jié)果看出,并沒有發(fā)生死鎖,一個賬戶先獲取了兩把鎖,完成轉(zhuǎn)賬后,另一個賬號再獲取到兩把鎖,完成轉(zhuǎn)賬。

上面的demo比較見到,如果賬號沒獲取到鎖,會一直while循環(huán)等待,可以優(yōu)化為notify/wait的方式。
2、?破壞不可搶占條件
破壞不搶占條件,需要發(fā)生死鎖的線程能夠主動釋放它占有的資源,但使用synchronized是做不到的。原因為synchronized申請不到資源時,線程直接進(jìn)入了阻塞狀態(tài),而線程進(jìn)入了阻塞狀態(tài)也就沒有辦法釋放它占有的資源了。


不過JDK中的Lock解決這個問題。


使用Lock類中的定時tryLock獲取鎖,可以指定一個超時時限(Timeout),在等待超過該時間后tryLock就會返回一個失敗信息,也會釋放其擁有的資源。
public class DeadLock3 { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) { Thread a = new Thread(new Lock1()); Thread b = new Thread(new Lock2()); a.start(); b.start(); }
static class Lock1 implements Runnable { @Override public void run() { try { System.out.println("Lock1 running"); while (true) { if (lock1.tryLock(1, TimeUnit.MILLISECONDS)) { System.out.println("Lock1 get lock1"); if (lock2.tryLock(1, TimeUnit.MILLISECONDS)) { System.out.println("Lock·get lock2"); return; } } } } catch (Exception e) { e.printStackTrace(); } finally { lock1.unlock(); lock2.unlock(); } } }
static class Lock2 implements Runnable { @Override public void run() { try { System.out.println("Lock2 running"); while (true) { if (lock1.tryLock(1, TimeUnit.MILLISECONDS)) { System.out.println("Lock2 get lock1"); if (lock2.tryLock(1, TimeUnit.MILLISECONDS)) { System.out.println("Lock2 get lock2"); return; } } } } catch (Exception e) { e.printStackTrace(); } finally { lock1.unlock(); lock2.unlock(); } } }}輸出結(jié)果如下:


圖5?破壞不可搶占條件輸出


從輸出結(jié)果看出,并沒有發(fā)生死鎖,一個賬戶先嘗試獲取兩把鎖,如果超時沒有獲取到,就會下次重試再去獲取,直到獲取成功。
3、破壞循環(huán)等待條件


破壞循環(huán)等待,就是要對系統(tǒng)中的資源進(jìn)行統(tǒng)一編號,進(jìn)程必須按照資源的編號順序提出。這樣做就能保證系統(tǒng)不出現(xiàn)死鎖。這就是“資源有序分配法”。代碼如下:
class Account { private int id; private int balance; void transfer(Account target, int amt){ Account left = this; Account right = target; if (this.id > target.id) { left = target; right = this; } synchronized(left){ synchronized(right){ if (this.balance > amt){ this.balance -= amt; target.balance = amt; } } } } }總結(jié):

文章主要講了死鎖發(fā)生的原因以及解決方法,但我們平時寫的代碼,可能邏輯比這里的例子要復(fù)雜很多,如果產(chǎn)生了死鎖,可能會比較難以定位到,所以我們平時寫代碼時,盡量不要把多個鎖交織在一起。

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
關(guān)閉
關(guān)閉