線程之活躍度失敗(死鎖、活鎖、饑餓)

線程活躍度

活躍度問題是指線程或進程長時間得不到cpu占用。《Java并發(fā)編程實戰(zhàn)》中提到,無論執(zhí)行計算密集操作還是執(zhí)行某個可能阻塞的操作,如果持有鎖的時間過長,都會帶來活躍性或性能問題。

活躍度失敗有那幾種

  • 死鎖也就是互相等著對方釋放資源,結(jié)果誰也得不到。

  • 活鎖可能發(fā)生讓某一個線程一直處于等待狀態(tài),其他線程都可以調(diào)用到。

  • 饑餓我就感覺用搶占式說好說,每次來就執(zhí)行優(yōu)先級高的,那么優(yōu)先級低的可能永遠執(zhí)行不到。

死鎖

多線程以及多進程改善了系統(tǒng)資源的利用率并提高了系統(tǒng) 的處理能力。然而,并發(fā)執(zhí)行也帶來了新的問題——死鎖。死鎖是指多個線程因競爭資源而造成的一種僵局(互相等待),若無外力作用,這些進程都將無法向前推進。

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

  • 系統(tǒng)資源的競爭

    通常系統(tǒng)中擁有的不可剝奪資源,其數(shù)量不足以滿足多個進程運行的需要,使得進程在 運行過程中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有對不可剝奪資源的競爭 才可能產(chǎn)生死鎖,對可剝奪資源的競爭是不會引起死鎖的。

  • 進程推進順序非法

    進程在運行過程中,請求和釋放資源的順序不當,也同樣會導(dǎo)致死鎖。例如,并發(fā)進程 P1、P2分別保持了資源R1、R2,而進程P1申請資源R2,進程P2申請資源R1時,兩者都 會因為所需資源被占用而阻塞。

    信號量使用不當也會造成死鎖。進程間彼此相互等待對方發(fā)來的消息,結(jié)果也會使得這 些進程間無法繼續(xù)向前推進。例如,進程A等待進程B發(fā)的消息,進程B又在等待進程A 發(fā)的消息,可以看出進程A和B不是因為競爭同一資源,而是在等待對方的資源導(dǎo)致死鎖。

死鎖產(chǎn)生的必要四個條件

產(chǎn)生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發(fā)生。

  • 互斥條件:進程要求對所分配的資源(如打印機)進行排他性控制,即在一段時間內(nèi)某 資源僅為一個進程所占有。此時若有其他進程請求該資源,則請求進程只能等待。

  • 不剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能 由獲得該資源的進程自己來釋放(只能是主動釋放)。

  • 請求和保持條件:進程已經(jīng)保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他進程占有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。

  • 循環(huán)等待條件:存在一種進程資源的循環(huán)等待鏈,鏈中每一個進程已獲得的資源同時被 鏈中下一個進程所請求。即存在一個處于等待狀態(tài)的進程集合{Pl, P2, ..., pn},其中Pi等 待的資源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的資源被P0占有。

寫一個死鎖的例子

/**
 * @Author 安仔夏天勤奮
 * Create Date is  2019/4/29
 * Des
 */
public class LockThread1 implements Runnable {
    @Override
    public void run() {
        ////模擬線程1占用資源1并申請獲得資源2的鎖
        try {
            System.out.println("LockThread1 is running");
            synchronized (ThreadResource.resource1) {
                System.out.println("LockThread1 lock resource1");
                Thread.sleep(2000);//休眠2s等待線程2鎖定資源2
                synchronized (ThreadResource.resource2){
                    System.out.println("LockThread1 lock resource2");
                }
                System.out.println("LockThread1 release resource2");
            }
            System.out.println("LockThread1 release resource1");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        System.out.println("LockThread1 is stop");
    }
}

/**
 * @Author 安仔夏天勤奮
 * Create Date is  2019/4/29
 * Des
 */
public class LockThread2 implements Runnable {
    @Override
    public void run() {
        //模擬線程2占用資源2并申請獲得資源1的鎖:
        try {
            System.out.println("LockThread2 is running");
            synchronized (ThreadResource.resource2) {
                System.out.println("LockThread2 lock resource2");
                Thread.sleep(2000);//休眠2s等待線程1鎖定資源1
                synchronized (ThreadResource.resource1) {
                    System.out.println("LockThread2 lock resource1");
                }
                System.out.println("LockThread2 release resource1");
            }
            System.out.println("LockThread2 release resource2");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        System.out.println("LockThread2 is stop");
    }
}

/**
 * @Author 安仔夏天勤奮
 * Create Date is  2019/4/29
 * Des
 */
public class ThreadResource {
    public static Object resource1 = new Object();
    public static Object resource2 = new Object();
}

/**
 * @Author 安仔夏天勤奮
 * Create Date is  2019/4/29
 * Des
 */
public class DeadLock{
    public static void main(String[] args) {
        new Thread(new LockThread1()).start();
        new Thread(new LockThread2()).start();
    }
}

運行結(jié)果

Thread1 is running
Thread2 is running
Thread1 lock resource1
Thread2 lock resource2
?
并且程序一直無法結(jié)束。這就是由于線程1占用了資源1,此時線程2已經(jīng)占用資源2,這個時候線程1想要使用資源2,線程2想要使用資源1。兩個線程都無法讓步,導(dǎo)致程序死鎖。

如何避免死鎖

死鎖是可以避免的。用于避免死鎖的技術(shù)三種方式:

  • 加鎖順序(線程按照一定的順序加鎖)

  • 加鎖時限(線程嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,并釋放自己占有的鎖)

  • 死鎖檢測

由上面的例子可以看出當線程在同步某個對象里,再去鎖定另外一個對象的話,就和容易發(fā)生死鎖的情況。最好是線程每次只鎖定一個對象并且在鎖定該對象的過程中不再去鎖定其他的對象,這樣就不會導(dǎo)致死鎖了。比如將以上的線程改成下面這種寫法就可以避免死鎖

/**
 * @Author 安仔夏天勤奮
 * Create Date is  2019/4/29
 * Des
 */
public class LockThread1 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("LockThread1 is running");
            synchronized (ThreadResource.resource1) {
                System.out.println("LockThread1 lock resource1");
                Thread.sleep(2000);//休眠2s等待線程2鎖定資源2
            }
            System.out.println("LockThread1 release resource1");
            synchronized (ThreadResource.resource2) {
                System.out.println("LockThread1 lock resource2");
            }
            System.out.println("LockThread1 release resource2");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        System.out.println("LockThread1 is stop");
}

/**
 * @Author 安仔夏天勤奮
 * Create Date is  2019/4/29
 * Des
 */
public class LockThread2 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("LockThread2 is running");
            synchronized (ThreadResource.resource2) {
                System.out.println("LockThread2 lock resource2");
                Thread.sleep(2000);//休眠2s等待線程1鎖定資源1
            }
            System.out.println("LockThread2 release resource2");
            synchronized (ThreadResource.resource1) {
                System.out.println("LockThread2 lock resource1");
            }
            System.out.println("LockThread2 release resource1");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        System.out.println("LockThread2 is stop");
    }
}

運行結(jié)果

LockThread1 is running
LockThread1 lock resource1
LockThread2 is running
LockThread2 lock resource2
LockThread1 release resource1
LockThread1 lock resource2
LockThread1 release resource2
LockThread1 is stop
LockThread2 release resource2
LockThread2 lock resource1
LockThread2 release resource1
LockThread2 is stop

如果需要同時去鎖定兩個對象,可以根據(jù)加鎖順序定義一個先后的規(guī)則。按照上面的例子,需要同時鎖定兩個資源,可以根據(jù)資源的hashcode值大小來判斷先后鎖定順序。代碼如下:

public class LockThread3 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("LockThread3 is running");
            if ( ThreadResource.resource1.hashCode() > ThreadResource.resource2.hashCode() ) {
                //先鎖定resource1
                synchronized (ThreadResource.resource1) {
                    System.out.println("LockThread3 lock resource1");
                    Thread.sleep(2000);
                    synchronized (ThreadResource.resource2) {
                        System.out.println("LockThread3 lock resource2");
                    }
                    System.out.println("LockThread3 release resource2");
                }
                System.out.println("LockThread3 release resource1");
            }else {
                //先鎖定resource2
                synchronized (ThreadResource.resource2) {
                    System.out.println("LockThread3 lock resource2");
                    Thread.sleep(2000);
                    synchronized (ThreadResource.resource1) {
                        System.out.println("LockThread3 lock resource1");
                    }
                    System.out.println("LockThread3 release resource1");
                }
                System.out.println("LockThread3 release resource2");
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        System.out.println("LockThread3 is stop");
    }
}

死鎖更詳細可參照

活鎖

活鎖是指線程1可以使用資源,但它很禮貌,讓其他線程先使用資源,線程2也可以使用資源,但它很紳士,也讓其他線程先使用資源。這樣你讓我,我讓你,最后兩個線程都無法使用資源。

活鎖不會被阻塞,而是不停檢測一個永遠不可能為真的條件。除去進程本身持有的資源外,活鎖狀態(tài)的進程會持續(xù)耗費寶貴的CPU時間。

舉個例子,兩個人在走廊上碰見,大家都互相很有禮貌,互相禮讓,A從左到右,B也從從左轉(zhuǎn)向右,發(fā)現(xiàn)又擋住了地方,繼續(xù)轉(zhuǎn)換方向,但又碰到了,反反復(fù)復(fù),一直沒有機會運行下去。

活鎖例子

活鎖的解決方法

  • 調(diào)整重試機制。

  • 引入一些隨機性。

活鎖和死鎖的區(qū)別

  • 活鎖的實體是在不斷的改變狀態(tài),所謂的“活”, 而處于死鎖的實體表現(xiàn)為等待。

  • 活鎖有可能自行解開,死鎖則不能。

饑餓

饑餓是指如果線程T1占用了資源R,線程T2又請求封鎖R,于是T2等待。T3也請求資源R,當T1釋放了R上的封鎖后,系統(tǒng)首先批準了T3的請求,T2仍然等待。然后T4又請求封鎖R,當T3釋放了R上的封鎖之后,系統(tǒng)又批準了T4的請求......,T2可能永遠等待。

線程長時間無法獲得共享資源從而繼續(xù)相繼的處理。這種情況經(jīng)常發(fā)生在當共享資源被“貪婪”線程長時間占據(jù)時。假設(shè)一個對象提供的互斥方法需要很長時間處理才能返回,然而如果某線程老是頻繁激活這個方法,那么其他需要訪問該對象的線程就會被長時間阻塞,而處于饑餓狀態(tài)。

饑餓.png

Java中的讀寫鎖的實現(xiàn)類ReentranctReadWriteLock,在默認使用非公平模式(不是先來先處理的模式)的情況下,如果某個線程想要讀取資源,只要沒有線程正在對該資源進行寫操作且沒有線程請求對該資源的寫操作即可。如果讀操作發(fā)生的比較頻繁,我們又沒有提升寫操作的優(yōu)先級,那么就會產(chǎn)生“饑餓”現(xiàn)象。請求寫操作的線程會一直阻塞,直到所有的讀線程都從ReentranctReadWriteLock上解鎖了。如果一直保證新線程的讀操作權(quán)限,那么等待寫操作的線程就會一直阻塞下去,結(jié)果就發(fā)生了“饑餓”。

java代碼會引起這種類型的饑餓

synchronized(obj) {
 while (true) {
 // .... infinite loop
 }
}

優(yōu)先級引起也會引起線程饑餓

高優(yōu)先級線程吞噬所有的低優(yōu)先級線程的CPU時間。例如在java中調(diào)用了Thread.setPriority方法設(shè)置了線程優(yōu)先級,優(yōu)先級低的線程始終得不到執(zhí)行的機會,雖然線程優(yōu)先級對于不同操作系統(tǒng)的實現(xiàn)方式不一樣,即便設(shè)置了優(yōu)先級也不一定會有效果,但還是有可能會出現(xiàn)這種情況。

饑餓的解決辦法有

  • 提升寫請求的優(yōu)先級或者采用公平策略。

  • 在synchronized方法或者塊中避免無限循環(huán)。

  • 采用線程默認的優(yōu)先級。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Java-Review-Note——4.多線程 標簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,780評論 2 17
  • 1、競態(tài)條件: 定義:競態(tài)條件指的是一種特殊的情況,在這種情況下各個執(zhí)行單元以一種沒有邏輯的順序執(zhí)行動作,從而導(dǎo)致...
    Hughman閱讀 1,442評論 0 7
  • 1.解決信號量丟失和假喚醒 public class MyWaitNotify3{ MonitorObject m...
    Q羅閱讀 1,017評論 0 1
  • 又來到了一個老生常談的問題,應(yīng)用層軟件開發(fā)的程序員要不要了解和深入學習操作系統(tǒng)呢? 今天就這個問題開始,來談?wù)劜?..
    tangsl閱讀 4,332評論 0 23
  • ## 研究是一種能力 在我讀研究生的時候,才有了第一次真正接觸到“研究能力”這個概念。和同學們相比,連最初到問題如...
    都市牛閱讀 295評論 0 0

友情鏈接更多精彩內(nèi)容