日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

死鎖

 

什么是死鎖(dead-lock)?如何避免死鎖詳解

思維導圖

是什么

線程死鎖是指由于兩個或者多個線程互相持有對方所需要的資源,導致這些線程處于等待狀態,無法前往執行。

當線程進入對象的synchronized代碼塊時,便占有了資源,直到它退出該代碼塊或者調用wait方法,才釋放資源,在此期間,其他線程將不能進入該代碼塊。

當線程互相持有對方所需要的資源時,會互相等待對方釋放資源,如果線程都不主動釋放所占有的資源,將產生死鎖。

發生的必要條件

當然死鎖的產生是必須要滿足一些特定條件的:

1.互斥條件:進程對于所分配到的資源具有排它性,即一個資源只能被一個進程占用,直到被該進程釋放

2.請求和保持條件:一個進程因請求被占用資源而發生阻塞時,對已獲得的資源保持不放。

3.不剝奪條件:任何一個資源在沒被該進程釋放之前,任何其他進程都無法對他剝奪占用

4.循環等待條件:當發生死鎖時,所等待的進程必定會形成一個環路(類似于死循環),造成永久阻塞。

死鎖的預防

前面介紹了死鎖發生時的四個必要條件,只要破壞這四個必要條件中的任意一個條件,死鎖就不會發生。這就為我們解決死鎖問題提供了可能。一般地,解決死鎖的方法分為死鎖的預防,避免,檢測與恢復三種(注意:死鎖的檢測與恢復是一個方法)。我們將在下面分別加以介紹。

死鎖的預防是保證系統不進入死鎖狀態的一種策略。它的基本思想是要求進程申請資源時遵循某種協議,從而打破產生死鎖的四個必要條件中的一個或幾個,保證系統不會進入死鎖狀態。

1. 打破互斥條件。

即允許進程同時訪問某些資源。但是,有的資源是不允許被同時訪問的,像打印機等等,這是由資源本身的屬性所決定的。

所以,這種辦法并無實用價值。

2. 打破不可搶占條件。

即允許進程強行從占有者那里奪取某些資源。

就是說,當一個進程已占有了某些資源,它又申請新的資源,但不能立即被滿足時,它必須釋放所占有的全部資源,以后再重新申請。

它所釋放的資源可以分配給其它進程。這就相當于該進程占有的資源被隱蔽地強占了。

這種預防死鎖的方法實現起來困難,會降低系統性能。

3. 打破占有且申請條件。

可以實行資源預先分配策略。即進程在運行前一次性地向系統申請它所需要的全部資源。

如果某個進程所需的全部資源得不到滿足,則不分配任何資源,此進程暫不運行。

只有當系統能夠滿足當前進程的全部資源需求時,才一次性地將所申請的資源全部分配給該進程。

由于運行的進程已占有了它所需的全部資源,所以不會發生占有資源又申請資源的現象,因此不會發生死鎖。

但是,這種策略也有如下缺點:

(1)在許多情況下,一個進程在執行之前不可能知道它所需要的全部資源。這是由于進程在執行時是動態的,不可預測的;

(2)資源利用率低。無論所分資源何時用到,一個進程只有在占有所需的全部資源后才能執行。即使有些資源最后才被該進程用到一次,但該進程在生存期間卻一直占有它們,造成長期占著不用的狀況。這顯然是一種極大的資源浪費;

(3)降低了進程的并發性。因為資源有限,又加上存在浪費,能分配到所需全部資源的進程個數就必然少了。

4. 打破循環等待條件,實行資源有序分配策略。

采用這種策略,即把資源事先分類編號,按號分配,使進程在申請,占用資源時不會形成環路。

所有進程對資源的請求必須嚴格按資源序號遞增的順序提出。

進程占用了小號資源,才能申請大號資源,就不會產生環路,從而預防了死鎖。

這種策略與前面的策略相比,資源的利用率和系統吞吐量都有很大提高,但是也存在以下缺點:

(1)限制了進程對資源的請求,同時給系統中所有資源合理編號也是件困難事,并增加了系統開銷;

(2)為了遵循按編號申請的次序,暫不使用的資源也需要提前申請,從而增加了進程對資源的占用時間。

什么是死鎖(dead-lock)?如何避免死鎖詳解

死鎖

死鎖的避免

上面我們講到的死鎖預防是排除死鎖的靜態策略,它使產生死鎖的四個必要條件不能同時具備,從而對進程申請資源的活動加以限制, 以保證死鎖不會發生。

下面我們介紹排除死鎖的動態策略--死鎖的避免,它不限制進程有關申請資源的命令, 而是對進程所發出的每一個申請資源命令加以動態地檢查,并根據檢查結果決定是否進行資源分配。

就是說,在資源分配過程中若預測有發生死鎖的可能性,則加以避免。這種方法的關鍵是確定資源分配的安全性。

1.安全序列

我們首先引入安全序列的定義:所謂系統是安全的,是指系統中的所有進程能夠按照某一種次序分配資源,并且依次地運行完畢,這種進程序列{P1,P2,...,Pn}就是安全序列。如果存在這樣一個安全序列,則系統是安全的;如果系統不存在這樣一個安全序列,則系統是不安全的。

安全序列{P1,P2,...,Pn}是這樣組成的:若對于每一個進程Pi,它需要的附加資源可以被系統中當前可用資源加上所有進程Pj當前占有資源之和所滿足,則{P1,P2,...,Pn}為一個安全序列,這時系統處于安全狀態,不會進入死鎖狀態。

雖然存在安全序列時一定不會有死鎖發生,但是系統進入不安全狀態(四個死鎖的必要條件同時發生)也未必會產生死鎖。

當然,產生死鎖后,系統一定處于不安全狀態。

2.銀行家算法

這是一個著名的避免死鎖的算法,是由Dijstra首先提出來并加以解決的。

  • 背景知識

一個銀行家如何將一定數目的資金安全地借給若干個客戶,使這些客戶既能借到錢完成要干的事,同時銀行家又能收回全部資金而不至于破產,這就是銀行家問題。這個問題同操作系統中資源分配問題十分相似:銀行家就像一個操作系統,客戶就像運行的進程,銀行家的資金就是系統的資源。

  • 問題的描述

一個銀行家擁有一定數量的資金,有若干個客戶要貸款。每個客戶須在一開始就聲明他所需貸款的總額。

若該客戶貸款總額不超過銀行家的資金總數,銀行家可以接收客戶的要求。客

戶貸款是以每次一個資金單位(如1萬RMB等)的方式進行的,客戶在借滿所需的全部單位款額之前可能會等待, 但銀行家須保證這種等待是有限的,可完成的。

例如:有三個客戶C1,C2,C3,向銀行家借款,該銀行家的資金總額為10個資金單位,其中C1客戶要借9各資金單位, C2客戶要借3個資金單位,C3客戶要借8個資金單位,總計20個資金單位。

某一時刻的狀態如圖所示。

什么是死鎖(dead-lock)?如何避免死鎖詳解

 

銀行家算法

對于a圖的狀態,按照安全序列的要求,我們選的第一個客戶應滿足該客戶所需的貸款小于等于銀行家當前所剩余的錢款,可以看出只有C2客戶能被滿足:C2客戶需1個資金單位,小銀行家手中的2個資金單位,于是銀行家把1個資金單位借給C2客戶,使之完成工作并歸還所借的3個資金單位的錢,進入b圖。同理,銀行家把4個資金單位借給C3客戶,使其完成工作,在c圖中,只剩一個客戶C1,它需7個資金單位,這時銀行家有8個資金單位,所以C1也能順利借到錢并完成工作。最后(見圖d)銀行家收回全部10個資金單位,保證不賠本。那麼客戶序列{C1,C2,C3}就是個安全序列,按照這個序列貸款,銀行家才是安全的。否則的話,若在圖b狀態時,銀行家把手中的4個資金單位借給了C1,則出現不安全狀態:這時C1,C3均不能完成工作,而銀行家手中又沒有錢了,系統陷入僵持局面,銀行家也不能收回投資。

綜上所述,銀行家算法是從當前狀態出發,逐個按安全序列檢查各客戶誰能完成其工作,然后假定其完成工作且歸還全部貸款,再進而檢查下一個能完成工作的客戶,......。如果所有客戶都能完成工作,則找到一個安全序列,銀行家才是安全的。

從上面分析看出,銀行家算法允許死鎖必要條件中的互斥條件,占有且申請條件,不可搶占條件的存在,這樣,它與預防死鎖的幾種方法相比較,限制條件少了,資源利用程度提高了。

這是該算法的優點。其缺點是:

〈1〉這個算法要求客戶數保持固定不變,這在多道程序系統中是難以做到的。

〈2〉這個算法保證所有客戶在有限的時間內得到滿足,但實時客戶要求快速響應,所以要考慮這個因素。

〈3〉由于要尋找一個安全序列,實際上增加了系統的開銷。

死鎖的檢測與恢復

一般來說,由于操作系統有并發,共享以及隨機性等特點,通過預防和避免的手段達到排除死鎖的目的是很困難的。這需要較大的系統開銷,而且不能充分利用資源。為此,一種簡便的方法是系統為進程分配資源時,不采取任何限制性措施,但是提供了檢測和解脫死鎖的手段:能發現死鎖并從死鎖狀態中恢復出來。因此,在實際的操作系統中往往采用死鎖的檢測與恢復方法來排除死鎖。

死鎖檢測與恢復是指系統設有專門的機構,當死鎖發生時,該機構能夠檢測到死鎖發生的位置和原因,并能通過外力破壞死鎖發生的必要條件,從而使得并發進程從死鎖狀態中恢復出來。

死鎖的檢測

圖中所示為一個小的死鎖的例子。這時進程P1占有資源R1而申請資源R2,進程P2占有資源R2而申請資源R1,按循環等待條件,進程和資源形成了環路,所以系統是死鎖狀態。進程P1,P2是參與死鎖的進程。

下面我們再來看一看死鎖檢測算法。算法使用的數據結構是如下這些:

占有矩陣A:n*m階,其中n表示并發進程的個數,m表示系統的各類資源的個數,這個矩陣記錄了每一個進程當前占有各個資源類中資源的個數。

申請矩陣R:n*m階,其中n表示并發進程的個數,m表示系統的各類資源的個數,這個矩陣記錄了每一個進程當前要完成工作需要申請的各個資源類中資源的個數。

空閑向量T:記錄當前m個資源類中空閑資源的個數。

完成向量F:布爾型向量值為真(true)或假(false),記錄當前n個并發進程能否進行完。為真即能進行完,為假則不能進行完。

臨時向量W:開始時W:=T。

算法步驟:

(1)W:=T,
  
對于所有的i=1,2,...,n,
  
如果A[i]=0,則F[i]:=true;否則,F[i]:=false
  
(2)找滿足下面條件的下標i:
  
F[i]:=false并且R[i]〈=W
  
如果不存在滿足上面的條件i,則轉到步驟(4)。
  
(3)W:=W+A[i]
  
F[i]:=true
  
轉到步驟(2)
  
(4)如果存在i,F[i]:=false,則系統處于死鎖狀態,且Pi進程參與了死鎖。什麼時候進行死鎖的檢測取決于死鎖發生的頻率。如果死鎖發生的頻率高,那麼死鎖檢測的頻率也要相應提高,這樣一方面可以提高系統資源的利用率,一方面可以避免更多的進程卷入死鎖。如果進程申請資源不能滿足就立刻進行檢測,那麼每當死鎖形成時即能被發現,這和死鎖避免的算法相近,只是系統的開銷較大。為了減小死鎖檢測帶來的系統開銷,一般采取每隔一段時間進行一次死鎖檢測,或者在CPU的利用率降低到某一數值時,進行死鎖的檢測。 

死鎖的恢復

一旦在死鎖檢測時發現了死鎖,就要消除死鎖,使系統從死鎖狀態中恢復過來。

(1)最簡單,最常用的方法就是進行系統的重新啟動,不過這種方法代價很大,它意味著在這之前所有的進程已經完成的計算工作都將付之東流,包括參與死鎖的那些進程,以及未參與死鎖的進程。

(2)撤消進程,剝奪資源。終止參與死鎖的進程,收回它們占有的資源,從而解除死鎖。這時又分兩種情況:一次性撤消參與死鎖的全部進程,剝奪全部資源;或者逐步撤消參與死鎖的進程,逐步收回死鎖進程占有的資源。

一般來說,選擇逐步撤消的進程時要按照一定的原則進行,目的是撤消那些代價最小的進程,比如按進程的優先級確定進程的代價;考慮進程運行時的代價和與此進程相關的外部作業的代價等因素。

此外,還有進程回退策略,即讓參與死鎖的進程回退到沒有發生死鎖前某一點處,并由此點處繼續執行,以求再次執行時不再發生死鎖。雖然這是個較理想的辦法,但是操作起來系統開銷極大,要有堆棧這樣的機構記錄進程的每一步變化,以便今后的回退,有時這是無法做到的。

死鎖的場景

獲取鎖的順序導致的死鎖

public class DeadLockDemo extends Thread {

    private final String firstLock;

    private final String secondLock;

    public DeadLockDemo(String t1, String t2) {
        this.firstLock = t1;
        this.secondLock = t2;
    }

    @Override
    public void run() {
        synchronized (firstLock) {
            // 成功占有 firstLock
            System.out.println(Thread.currentThread().getName() + " success locked " + firstLock);
            // 休眠一段時間
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 嘗試占有 secondLock,如果不能占有,該線程會一直等到
            synchronized (secondLock) {
                System.out.println(Thread.currentThread().getName() + " success locked " + secondLock);
            }
        }
    }

    
}
  • main()
public static void main(String[] args) {
    final String resource1 = "resource1";
    final String resource2 = "resource2";

    // 兩個線程獲取鎖的順序相反
    new DeadLockDemo(resource1, resource2).start();
    new DeadLockDemo(resource2, resource1).start();
}

日志信息

Thread-0 success locked resource1
Thread-1 success locked resource2

然后進入死鎖狀態。

遞歸死鎖

所謂遞歸函數就是自調用函數,在函數體內直接或間接的調用自己,即函數的嵌套是函數本身。

遞歸方式有兩種:直接遞歸和間接遞歸,直接遞歸就是在函數中出現調用函數本身。間接遞歸,指函數中調用了其他函數,而該其他函數又調用了本函數。 那什么時候使用遞歸呢?

一般來說當你要在某段代碼邏輯中使用循環迭代的時候但是迭代的次數在迭代之前無法知曉的情況下使用遞歸。

打個比方你要在一個文件夾中查找某個文件,而這個文件夾底下有N多子文件夾和文件,當你在不知道有多少層文件夾和文件的情況下你就得用到遞歸了。

遞歸的優點就是讓代碼顯得很簡潔,同時有些應用場景不得不使用遞歸比如前面說的找文件。

遞歸是個好東西但是在某些時候也會給你帶來一些麻煩。

比如在多線程的環境下使用遞歸,遇到了多線程那么就不得不面對同步的問題。

而遞歸程序遇到同步的時候很容易出問題。

public class Test {  
    public void recursive(){  
        this.businessLogic();  
    }  
    public synchronized void businessLogic(){  
        System.out.println("處理業務邏輯");  
        this.recursive();  
    }  
}  

多線程的遞歸就是指遞歸鏈中的某個方法由另外一個線程來操作,以下代碼的意思都是這個意思即調用recursive()和businessLogic()并非一個線程

(如果是在一個線程中就不存在死鎖問題,例如上面的recursive變成private就不存在問題。)

以上這段代碼就是個能形成死鎖的代碼,事實上這個“synchronized”放在“businessLogic()”和“recursive()”都會形成死鎖,并且是多線程的情況下就會鎖住!

他的邏輯順序是先執行recursive()方法然后接下來執行businessLogic()方法同時將businessLogic()方法鎖住, 接下來程序進入businessLogic()方法內部執行完打印語句后開始執行recursive(), 進入recursive()后準備執行businessLogic(),等等問題來了!

之前執行的businessLogic()的鎖還沒有放開這次又執行到這里了,當然是過不去的了,形成了死鎖!

從這個例子我們總結出來一個規律就是在遞歸的時候在遞歸鏈上面的方法上加鎖肯定會出現死鎖(所謂遞歸鏈就是指recursive()鏈向businessLogic(),businessLogic()又鏈回recursive()),

解決這個問題的方法就是避免在遞歸鏈上加鎖,

請看以下的例子

public class Test {  
    public void recursive(){  
        this.businessLogic();  
    }  
    public  void businessLogic(){  
        System.out.println("處理業務邏輯");  
        this.saveToDB();  
        this.recursive();  
    }  
    public synchronized void saveToDB(){  
        System.out.println("保存到數據庫");  
    }  
}  

saveToDB()不在這條遞歸鏈上面自然不會出現死鎖,所以說在遞歸中加鎖是件很危險的事情,實在逃不過要加鎖就加在最小的粒度的程序代碼上以減小死鎖的概率。

動態順序死鎖

// 轉賬
public static void transferMoney(Account fromaccount,
                                 Account toAccount,
                                 DollarAmount amount)
        throws InsufficientFundsException {

    // 鎖定匯賬賬戶
    synchronized (fromAccount) {
        // 鎖定來賬賬戶
        synchronized (toAccount) {

            // 判余額是否大于0
            if (fromAccount.getBalance().compareTo(amount) < 0) {
                throw new InsufficientFundsException();
            } else {

                // 匯賬賬戶減錢
                fromAccount.debit(amount);

                // 來賬賬戶增錢
                toAccount.credit(amount);
            }
        }
    }
}

上面的代碼看起來是沒有問題的:鎖定兩個賬戶來判斷余額是否充足才進行轉賬!

但是,同樣有可能會發生死鎖:

如果兩個線程同時調用transferMoney()

線程A從X賬戶向Y賬戶轉賬

線程B從賬戶Y向賬戶X轉賬

那么就會發生死鎖。

協作對象之間發生死鎖

public class CooperatingDeadlock {
    // Warning: deadlock-prone!
    class Taxi {
        @GuardedBy("this") private Point location, destination;
        private final Dispatcher dispatcher;

        public Taxi(Dispatcher dispatcher) {
            this.dispatcher = dispatcher;
        }

        public synchronized Point getLocation() {
            return location;
        }

        // setLocation 需要Taxi內置鎖
        public synchronized void setLocation(Point location) {
            this.location = location;
            if (location.equals(destination))
                // 調用notifyAvailable()需要Dispatcher內置鎖
                dispatcher.notifyAvailable(this);
        }

        public synchronized Point getDestination() {
            return destination;
        }

        public synchronized void setDestination(Point destination) {
            this.destination = destination;
        }
    }

    class Dispatcher {
        @GuardedBy("this") private final Set<Taxi> taxis;
        @GuardedBy("this") private final Set<Taxi> availableTaxis;

        public Dispatcher() {
            taxis = new HashSet<Taxi>();
            availableTaxis = new HashSet<Taxi>();
        }

        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }

        // 調用getImage()需要Dispatcher內置鎖
        public synchronized Image getImage() {
            Image image = new Image();
            for (Taxi t : taxis)
                // 調用getLocation()需要Taxi內置鎖
                image.drawMarker(t.getLocation());
            return image;
        }
    }

    class Image {
        public void drawMarker(Point p) {
        }
    }
}

上面的getImage()和setLocation(Point location)都需要獲取兩個鎖的

并且在操作途中是沒有釋放鎖的 這就是隱式獲取兩個鎖(對象之間協作)..

這種方式也很容易就造成死鎖.....

避免死鎖:

在有些情況下死鎖是可以避免的。本文將展示三種用于避免死鎖的技術:

  1. 加鎖順序
  2. 加鎖時限
  3. 死鎖檢測
  4. 開放調用(針對對象之間協作造成的死鎖)

加鎖順序

當多個線程需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。

如果能確保所有的線程都是按照相同的順序獲得鎖,那么死鎖就不會發生。

看下面這個例子:

Thread 1:
  lock A 
  lock B

Thread 2:
   wait for A
   lock C (when A locked)

Thread 3:
   wait for A
   wait for B
   wait for C

如果一個線程(比如線程3)需要一些鎖,那么它必須按照確定的順序獲取鎖。它只有獲得了從順序上排在前面的鎖之后,才能獲取后面的鎖。

例如,線程2和線程3只有在獲取了鎖A之后才能嘗試獲取鎖C(譯者注:獲取鎖A是獲取鎖C的必要條件)。因為線程1已經擁有了鎖A,所以線程2和3需要一直等到鎖A被釋放。然后在它們嘗試對B或C加鎖之前,必須成功地對A加了鎖。

按照順序加鎖是一種有效的死鎖預防機制。

但是,這種方式需要你事先知道所有可能會用到的鎖(譯者注:并對這些鎖做適當的排序),但總有些時候是無法預知的。

加鎖時限

另外一個可以避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味著在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退并釋放所有已經獲得的鎖,然后等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,并且讓該應用在沒有獲得鎖的時候可以繼續運行(譯者注:加鎖超時后可以先繼續運行干點其它事情,再回頭來重復之前加鎖的邏輯)。

以下是一個例子,展示了兩個線程以不同的順序嘗試獲取相同的兩個鎖,在發生超時后回退并重試的場景:

Thread 1 locks A
Thread 2 locks B

Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked

Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.

Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,線程2比線程1早200毫秒進行重試加鎖,因此它可以先成功地獲取到兩個鎖。這時,線程1嘗試獲取鎖A并且處于等待狀態。當線程2結束時,線程1也可以順利的獲得這兩個鎖(除非線程2或者其它線程在線程1成功獲得兩個鎖之前又獲得其中的一些鎖)。

需要注意的是,由于存在鎖的超時,所以我們不能認為這種場景就一定是出現了死鎖。也可能是因為獲得了鎖的線程(導致其它線程超時)需要很長的時間去完成它的任務。

此外,如果有非常多的線程同一時間去競爭同一批資源,就算有超時和回退機制,還是可能會導致這些線程重復地嘗試但卻始終得不到鎖。如果只有兩個線程,并且重試的超時時間設定為0到500毫秒之間,這種現象可能不會發生,但是如果是10個或20個線程情況就不同了。因為這些線程等待相等的重試時間的概率就高的多(或者非常接近以至于會出現問題)。 (譯者注:超時和重試機制是為了避免在同一時間出現的競爭,但是當線程很多時,其中兩個或多個線程的超時時間一樣或者接近的可能性就會很大,因此就算出現競爭而導致超時后,由于超時時間一樣,它們又會同時開始重試,導致新一輪的競爭,帶來了新的問題。)

這種機制存在一個問題,在JAVA中不能對synchronized同步塊設置超時時間。你需要創建一個自定義鎖,或使用Java5中java.util.concurrent包下的工具。寫一個自定義鎖類不復雜,但超出了本文的內容。后續的Java并發系列會涵蓋自定義鎖的內容。

死鎖檢測

死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖并且鎖超時也不可行的場景。

每當一個線程獲得了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。

除此之外,每當有線程請求鎖,也需要記錄在這個數據結構中。

當一個線程請求鎖失敗時,這個線程可以遍歷鎖的關系圖看看是否有死鎖發生。

例如,線程A請求鎖7,但是鎖7這個時候被線程B持有,這時線程A就可以檢查一下線程B是否已經請求了線程A當前所持有的鎖。

如果線程B確實有這樣的請求,那么就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。

當然,死鎖一般要比兩個線程互相持有對方的鎖這種情況要復雜的多。

線程A等待線程B,線程B等待線程C,線程C等待線程D,線程D又在等待線程A。

線程A為了檢測死鎖,它需要遞進地檢測所有被B請求的鎖。

從線程B所請求的鎖開始,線程A找到了線程C,然后又找到了線程D,發現線程D請求的鎖被線程A自己持有著。這是它就知道發生了死鎖。

下面是一幅關于四個線程(A,B,C和D)之間鎖占有和請求的關系圖。像這樣的數據結構就可以被用來檢測死鎖。

什么是死鎖(dead-lock)?如何避免死鎖詳解

 

死鎖檢測

那么當檢測出死鎖時,這些線程該做些什么呢?

一個可行的做法是釋放所有鎖,回退,并且等待一段隨機的時間后重試。這個和簡單的加鎖超時類似,不一樣的是只有死鎖已經發生了才回退,而不會是因為加鎖的請求超時了。雖然有回退和等待,但是如果有大量的線程競爭同一批鎖,它們還是會重復地死鎖(編者注:原因同超時類似,不能從根本上減輕競爭)。

一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖一樣繼續保持著它們需要的鎖。如果賦予這些線程的優先級是固定不變的,同一批線程總是會擁有更高的優先級。為避免這個問題,可以在死鎖發生的時候設置隨機的優先級。

開放調用避免死鎖

在協作對象之間發生死鎖的例子中,主要是因為在調用某個方法時就需要持有鎖,并且在方法內部也調用了其他帶鎖的方法!

如果在調用某個方法時不需要持有鎖,那么這種調用被稱為開放調用!

我們可以這樣來改造:

同步代碼塊最好僅被用于保護那些涉及共享狀態的操作!

class CooperatingNoDeadlock {
    @ThreadSafe
    class Taxi {
        @GuardedBy("this") private Point location, destination;
        private final Dispatcher dispatcher;

        public Taxi(Dispatcher dispatcher) {
            this.dispatcher = dispatcher;
        }

        public synchronized Point getLocation() {
            return location;
        }

        public synchronized void setLocation(Point location) {
            boolean reachedDestination;

            // 加Taxi內置鎖
            synchronized (this) {
                this.location = location;
                reachedDestination = location.equals(destination);
            }
            // 執行同步代碼塊后完畢,釋放鎖

            if (reachedDestination)
                // 加Dispatcher內置鎖
                dispatcher.notifyAvailable(this);
        }

        public synchronized Point getDestination() {
            return destination;
        }

        public synchronized void setDestination(Point destination) {
            this.destination = destination;
        }
    }

    @ThreadSafe
    class Dispatcher {
        @GuardedBy("this") private final Set<Taxi> taxis;
        @GuardedBy("this") private final Set<Taxi> availableTaxis;

        public Dispatcher() {
            taxis = new HashSet<Taxi>();
            availableTaxis = new HashSet<Taxi>();
        }

        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }

        public Image getImage() {
            Set<Taxi> copy;

            // Dispatcher內置鎖
            synchronized (this) {
                copy = new HashSet<Taxi>(taxis);
            }
            // 執行同步代碼塊后完畢,釋放鎖

            Image image = new Image();
            for (Taxi t : copy)
                // 加Taix內置鎖
                image.drawMarker(t.getLocation());
            return image;
        }
    }

    class Image {
        public void drawMarker(Point p) {
        }
    }

}

小結

我們對死鎖是什么?常見的場景等做了解答。

結合實際工作中,如何分析發現,以及如何解決提供了理論和實際的方案。

理解發生的原因,才能更好地解決問題。

關于死鎖檢測,是個不錯的思路。是否可以針對這個專門寫一套框架?提供優雅的解決方案。小伙伴們有沒有想嘗試一下的?

希望本文對你有幫助,如果有其他想法的話,也可以評論區和大家分享哦。

分享到:
標簽:死鎖
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定