一文詳解,死鎖與解決方案(附源碼)
時間:2021-10-29 16:38:12
手機(jī)看文章
掃描二維碼
隨時隨地手機(jī)看文章
[導(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ā)生的原因去解決。
- 首先,“互斥”是沒有辦法避免的,你想從賬戶A轉(zhuǎn)賬到賬戶B,就必須加鎖,就沒法避免互斥的存在。
- 對于“占用且等待”這個條件,我們可以一次性申請所有的資源,這樣就不存在等待了。
- 對于“不可搶占”這個條件,占用部分資源的線程進(jìn)一步申請其他資源時,如果申請不到,可以在一定時間后,主動釋放它占有的資源,這樣就解決了不可搶占這個條件。
- 對于“循環(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)生了死鎖,可能會比較難以定位到,所以我們平時寫代碼時,盡量不要把多個鎖交織在一起。