死結發生在一個線程需要擷取多個資源的時候,這時由於兩個線程互相等待對方的資源而被阻塞,死結是最常見的活躍性問題。這裡先分析死結的情形:
假設當前情況是線程A已經擷取資源R1,線程B已經擷取資源R2,之後線程A嘗試擷取資源R2,這個時候因為資源R2已經被線程B獲得了,所以線程A只能阻塞直到線程B釋放資源R2。另一方面,線程B在已經獲得資源R2的前提下嘗試擷取由線程A持有的資源R1,那麼由於資源R1已經被線程A持有了,那麼線程B只能被阻塞直到線程A釋放資源R1。這樣線程A和線程B都在等待對方持有的資源,就造成了死結。這種情形由一個專業術語:順序死結。還有動態死結、協作死結以及資源死結,其實本質都一樣:都因為在等待被其他線程佔有的資源而造成整個程式無法繼續向下執行。
下面的程式示範了順序死結的情形:
package com.rhwayfun.concurrency;import java.text.DateFormat;import java.text.SimpleDateFormat;import java.util.Date;import java.util.concurrent.TimeUnit;/** * Created by rhwayfun on 16-4-3. */public class DeadLock { private static DateFormat format = new SimpleDateFormat("HH:mm:ss"); public synchronized void tryOther(DeadLock other) throws InterruptedException { System.out.println(Thread.currentThread().getName() + " enter tryOther method at " + format.format(new Date())); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + " tryOther method is about to invoke other method at " + format.format(new Date())); other.other(); } public synchronized void other() throws InterruptedException { System.out.println(Thread.currentThread().getName() + " enter other method at " + format.format(new Date())); TimeUnit.SECONDS.sleep(3); } public static void main(String[] args) throws InterruptedException { final DeadLock d1 = new DeadLock(); final DeadLock d2 = new DeadLock(); Thread t1 = new Thread(new Runnable() { public void run() { try { d1.tryOther(d2); } catch (InterruptedException e) { e.printStackTrace(); } } }, "threadA"); Thread t2 = new Thread(new Runnable() { public void run() { try { d2.tryOther(d1); } catch (InterruptedException e) { e.printStackTrace(); } } }, "threadB"); t1.start(); //讓threadA先運行一秒 TimeUnit.SECONDS.sleep(1); t2.start(); //運行10秒後嘗試中斷線程 TimeUnit.SECONDS.sleep(10); t1.interrupt(); t2.interrupt(); System.out.println("Is threadA is interrupted? " + t1.isInterrupted()); System.out.println("Is threadB is interrupted? " + t2.isInterrupted()); }}
運行結果如下:
可以看到在threadA和threadB並沒有進入到other方法中,說明程式發生了死結,threadA在等待threadB的資源,threadB在等待threadA的資源(這裡由於使用了同步方法,所以資源確切地說是指d1和d2的對象層級鎖)而導致了死結。雖然在Java中沒有很好避免死結的方法,但是在編程時遵循一些規則有利於最大限度降低死結的發生: 儘可能減小鎖的作用範圍,比如使用同步代碼塊而不使用同步方法 盡量不編寫在通時刻擷取多個鎖的代碼,因為在一個線程持有多個資源的時候很容易發生死結 根據情況將過大範圍的鎖進行切分,讓每個鎖的作用範圍減小,從而降低死結發生的機率。這以原則的典型應用是ConcurrentHashMap的鎖分段技術,具體可以參看這篇文章。
饑餓指的線程無法訪問到它需要的資源而不能繼續執行時,引發饑餓最常見資源就是CPU刻度。雖然在Thread API中由指定線程優先順序的機制,但是只能作為作業系統進行線程調度的一個參考,換句話說就是作業系統在進行線程調度是平台無關的,會儘可能提供公平的、活躍性良好的調度,那麼即使在程式中指定了線程的優先順序,也有可能在作業系統進行調度的時候映射到了同一個優先順序。通常情況下,不要區修改線程的優先順序,一旦修改程式的行為就會與平台相關,並且會導致饑餓問題的產生。在程式中使用的Thread.yield或者Thread.sleep表明該程式試圖客服優先順序調整問題,讓優先順序更低的線程擁有被CPU調度的機會。
活鎖指的是線程不斷重複執行相同的操作,但每次操作的結果都是失敗的。儘管這個問題不會阻塞線程,但是程式也無法繼續執行。活鎖通常發生在處理事務訊息的應用程式中,如果不能成功處理這個事務那麼事務將復原整個操作。解決活鎖的辦法是在每次重複執行的時候引入隨機機制,這樣由於出現的可能性不同使得程式可以繼續執行其他的任務。