Java多線程編程之死鎖

同步造成的死鎖問題是說兩(多)個線程互相佔用了對方所需要的資源,等待對方釋放資源僵持導致程序無人工干預不能結束的情況。
類似於哲學家就餐問題,比如共有3個資源被3個線程訪問,每個線程必須拿到2個資源才能保持正常運行。如果這3個線程在同一時刻取得了這3個資源,又沒有線程願意讓出資源,這時候就會出現3個線程互相等待其它線程釋放資源的情況而導致死鎖的出現(或者A依賴於B,B依賴於C,C依賴於A,它們互相取得了對方的資源)。

這裏參考一個已經寫好的例子對死鎖作分析。
Deadlock in Java Example

例:程序設計了一個SyncThread類,類中對對象申請了同步鎖(即當對象資源被佔用時其它線程不能取得該對象鎖。如果暫時無線程取得該對象鎖,在同一時間兩個線程進入了該同步塊中,則兩個對象均獲得了該鎖),而且一個對象鎖是在另一個對象鎖裏面鎖定的,work()方法休眠3000ms即代表佔用該對象資源3000ms。
新建三個Thread t1,t2,t3分步執行,初始化Runnable類SyncThread類的實例。t1,t2,t3分別申請(obj1,obj2),(obj2,obj3),(obj3,obj1),三個線程分別與其它線程有一個共享的資源。當一個線程獲取了兩個對象後會自動釋放其獲取的對象資源,如果一直獲取不到兩個對象資源就會僵持不下。

public class DeadLock {
    public static long begin;

    public static void main(String[] args) throws InterruptedException {
        begin = System.currentTimeMillis();
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();

        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");

        t1.start();
        Thread.sleep(2000);
        t2.start();
        Thread.sleep(2000);
        t3.start();
        long end = System.currentTimeMillis() - begin;
        System.out.println("end = " + end);
    }

}

class SyncThread implements Runnable {
    private Object obj1;
    private Object obj2;

    public SyncThread(Object o1, Object o2) {
        this.obj1 = o1;
        this.obj2 = o2;
    }

    @Override
    public void run() {

        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on " + obj1);
        synchronized (obj1) {
            System.out.println(name + " acquired lock on " + obj1);
            work();
            System.out.println(name + " acquiring lock on " + obj2);
            synchronized (obj2) {
                System.out.println(name + " acquired lock on " + obj2);
                work();
            }
            System.out.println(name + " released lock on " + obj2);
        }
        System.out.println(name + " released lock on " + obj1);
        System.out.println(name + " finished execution.");
    }

    private void work() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

查看此程序的結果
t1 acquiring lock on java.lang.Object@61a0353d
t1 acquired lock on java.lang.Object@61a0353d
t2 acquiring lock on java.lang.Object@11b75be2
t2 acquired lock on java.lang.Object@11b75be2
t1 acquiring lock on java.lang.Object@11b75be2
end = 4032
t3 acquiring lock on java.lang.Object@3219ab8d
t3 acquired lock on java.lang.Object@3219ab8d
t2 acquiring lock on java.lang.Object@3219ab8d
t3 acquiring lock on java.lang.Object@61a0353d

可以看出t1、t2、t3一直處於申請資源狀態的死鎖中,那麼爲什麼會出現這種狀態呢?可以分析下線程申請對象資源的時間軸:
這裏寫圖片描述
在0s時候,t1獲取obj1對象;
休眠2s後,即第2s,t2獲得了obj2對象;
當t1在執行work方法休眠3s後,即第3s,t1想要去獲取obj2對象,此時發現obj2對象已經被t2佔用了,此時t1被阻塞;
在第2s後休眠的2s後,即第4s,t3獲取了obj3對象;
第2s後的3s後(t2執行完work方法),即第5s,t2想要去獲取obj3對象,此時obj3對象已經被t3佔用了,此時t2被阻塞;
在t3獲取obj3對象並執行完work的時刻,即第7s,t3想要去獲取obj1對象,此時obj1對象正在阻塞中,被t1佔用,此時t3因爲不能獲取資源而陷入等待阻塞中。
這個時候t1,t2,t3互相佔用了對方需要的資源,從此陷入了無盡的等待中。

這個死鎖看起來是時間卡得比較緊,那麼如果將主函數中的

t2.start();
Thread.sleep(2000);

的休眠時間改成3000ms,即更改線程之間的開始運行時間

t2.start();
Thread.sleep(3000);

又會出現什麼樣的效果呢?

結果如下:
t1 acquiring lock on java.lang.Object@5d0769dd
t1 acquired lock on java.lang.Object@5d0769dd
t2 acquiring lock on java.lang.Object@1cf15b84
t2 acquired lock on java.lang.Object@1cf15b84
t1 acquiring lock on java.lang.Object@1cf15b84
t2 acquiring lock on java.lang.Object@334dcfad
end = 5016
t2 acquired lock on java.lang.Object@334dcfad
t3 acquiring lock on java.lang.Object@334dcfad
t3 acquired lock on java.lang.Object@334dcfad
t2 released lock on java.lang.Object@334dcfad
t2 released lock on java.lang.Object@1cf15b84
t2 finished execution.
t1 acquired lock on java.lang.Object@1cf15b84
t3 acquiring lock on java.lang.Object@5d0769dd
t1 released lock on java.lang.Object@1cf15b84
t3 acquired lock on java.lang.Object@5d0769dd
t1 released lock on java.lang.Object@5d0769dd
t1 finished execution.
t3 released lock on java.lang.Object@5d0769dd
t3 released lock on java.lang.Object@334dcfad
t3 finished execution.

申請資源的時間軸如下:
這裏寫圖片描述

分析和上面的類似,由於給了線程之間足夠多的時間過度去獲得資源和利用資源,可以使得線程能夠及時運行完並釋放對象,從而避免了死鎖的發生。

但是實際開發過程中我們很難預測到它們花費的具體時間,無法精確地通過它們的間隔來避免死鎖的發生,但是可以通過其它的方法來避免死鎖的發生。第一就是可以設置一個最長的等待時間,超過這個等待時間就釋放資源而不是一直佔用着不放,這個有點類似於時間片的功能;第二就是儘量不要互相佔用對方的資源(當然這個也得根據需求來設計);再者就是儘量不要把整個對象全給鎖住,可以鎖住更小的區域,可以考慮使用Lock實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章