當開發者在應用中使用了併發來提高性能的同時,開發者也須要注意線程之間有可能會相互阻塞。當整個應用執行的速度比預期要慢的時候,也就是應用沒有按照預期的執行時間執行完畢。在本章中,咱們來須要仔細分析可能會影響應用多線程的活性問題。java
死鎖的概念在軟件開發者中已經廣爲熟知了,甚至普通的計算機用戶也會常用這個概念,儘管不是在正確的情況下使用。嚴格來講,死鎖意味着兩個或者更多線程在等待另外一個線程釋放其鎖定的資源,而請求資源的線程自己也鎖定了對方線程所請求的資源。以下:sql
Thread 1: locks resource A, waits for resource B
Thread 2: locks resource B, waits for resource A
爲了更好的理解問題,參考一下以下的代碼:bash
public class Deadlock implements Runnable {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
private final Random random = new Random(System.currentTimeMillis());
public static void main(String[] args) {
Thread myThread1 = new Thread(new Deadlock(), "thread-1");
Thread myThread2 = new Thread(new Deadlock(), "thread-2");
myThread1.start();
myThread2.start();
}
public void run() {
for (int i = 0; i < 10000; i++) {
boolean b = random.nextBoolean();
if (b) {
System.out.println("[" + Thread.currentThread().getName() +
"] Trying to lock resource 1.");
synchronized (resource1) {
System.out.println("[" + Thread.currentThread().
getName() + "] Locked resource 1.");
System.out.println("[" + Thread.currentThread().
getName() + "] Trying to lock resource 2.");
synchronized (resource2) {
System.out.println("[" + Thread.
currentThread().getName() + "] Locked resource 2.");
}
}
} else {
System.out.println("[" + Thread.currentThread().getName() +
"] Trying to lock resource 2.");
synchronized (resource2) {
System.out.println("[" + Thread.currentThread().
getName() + "] Locked resource 2.");
System.out.println("[" + Thread.currentThread().
getName() + "] Trying to lock resource 1.");
synchronized (resource1) {
System.out.println("[" + Thread.
currentThread().getName() + "] Locked resource 1.");
}
}
}
}
}
}
從上面的代碼中能夠看出,兩個線程分別啓動,而且嘗試鎖定2個靜態的資源。但對於死鎖,咱們須要兩個線程的以不一樣順序鎖定資源,所以咱們利用隨機實例選擇線程要首先鎖定的資源。markdown
若是布爾變量b
爲true
,resource1
會鎖定,而後嘗試去得到resource2
的鎖。若是b
是false
,線程會優先鎖定resource2
,然而嘗試鎖定resource1
。程序不用一下子就會碰到死鎖問題,而後就會一直掛住,直到咱們結束了JVM纔會結束:多線程
[thread-1] Trying to lock resource 1. [thread-1] Locked resource 1. [thread-1] Trying to lock resource 2. [thread-1] Locked resource 2. [thread-2] Trying to lock resource 1. [thread-2] Locked resource 1. [thread-1] Trying to lock resource 2. [thread-1] Locked resource 2. [thread-2] Trying to lock resource 2. [thread-1] Trying to lock resource 1.
在上面的執行中,thread-1
持有了resource2
的鎖,等待resource1
的鎖,而線程thread-2
持有了resource1
的鎖,等待resource2
的鎖。併發
若是咱們將b
的值配置true
或者false
的話,是不會碰到死鎖的,由於執行的順序始終是一致的,那麼thread-1
和thread-2
請求鎖的順序始終是一致的。兩個線程都會以一樣的順序請求鎖,那麼最多會暫時阻塞一個線程,最終都可以順序執行。dom
大概來講,形成死鎖須要以下的一些條件:函數
儘管產生死鎖的條件看起來較多,可是在多線程應用中存在死鎖仍是比較常見的。開發者能夠經過打破死鎖構成的必要條件來避免死鎖的產生,參考以下:post
ReentrantLock
就提供了相似超時的方法。在一個更高級的應用中,開發者或許須要考慮實現一個檢測死鎖的系統。在這個系統中,來實現一些基於線程的監控,當前程獲取一個鎖,而且嘗試請求別的鎖的時候,都記錄日誌。若是以線程和鎖構成有向圖,開發者是可以檢測到2不一樣的線程持有資源而且同時請求另外的阻塞的資源的。若是開發者能夠檢測,並可以強制阻塞的線程釋放掉已經獲取的資源,就可以自動檢測到死鎖而且自動修復死鎖問題。性能
線程調度器會決定哪個處於RUNNABLE
狀態的線程會的執行順序。決定通常是基於線程的優先級的;所以,低優先級的線程會得到較少的CPU時間,而高優先級的線程會得到較多的CPU時間。固然,這種調度聽起來較爲合理,可是有的時候也會引發問題。若是老是執行高優先級的線程,那麼低優先級的線程就會沒法得到足夠的時間來執行,處於一種飢餓狀態。所以,建議開發者只在真的十分必要的時候纔去配置線程的優先級。
一個很複雜的線程飢餓的例子就是finalize()
方法。Java語言中的這一特性能夠用來進行垃圾回收,可是當開發者查看一下finalizer
線程的優先級,就會發現其運行的優先級不是最高的。所以,頗有可能finalize()
方法跟其餘方法比起來會執行更久。
另外一個執行時間的問題是,線程以何種順序經過同步代碼塊是沒有定義的。當不少並行線程須要經過封裝的同步代碼塊時,會有的線程等待的時間要比其它線程的時間更久才能進入同步代碼快。理論上,他們可能永遠沒法進入代碼塊。這個問題可使用公平鎖的方案來解決。公平鎖在選擇下個線程的時候會考慮到線程的等待時間。其中一個公平鎖的實現就是java.util.concurrent.locks.ReentrantLock
:
若是使用ReentrantLock
的以下構造函數:
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
傳入true
,那麼ReentrantLock
是一個公平鎖,是會容許線程按掛起順序來依次獲取鎖執行的。這樣能夠削減線程的飢餓,可是,並不能徹底解決飢餓的問題,畢竟線程的調度是由操做系統調度的。因此,ReentrantLock
類只考慮等待鎖的線程,調度上是沒法起做用的。舉個例子,儘管使用了公平鎖,可是操做系統會給低優先級的線程很短的執行時間。