Java線程和多線程(十五)——線程的活性
當開發人員在應用中使用了並發來提升性能的同一時候。開發人員也須要註意線程之間有可能會相互堵塞。
當整個應用運行的速度比預期要慢的時候,也就是應用沒有依照預期的運行時間運行完成。在本章中。我們來須要細致分析可能會影響應用多線程的活性問題。
死鎖
死鎖的概念在軟件開發人員中已經廣為熟知了,甚至普通的計算機用戶也會常常使用這個概念。雖然不是在正確的狀況下使用。嚴格來說,死鎖意味著兩個或者很多其它線程在等待還有一個線程釋放其鎖定的資源,而請求資源的線程本身也鎖定了對方線程所請求的資源。
例如以下:
Thread 1: locks resource A, waits for resource B
Thread 2: locks resource B, waits for resource A
為了更好的理解問題,參考一下例如以下的代碼:
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個靜態的資源。但對於死鎖。我們須要兩個線程的以不同順序鎖定資源,因此我們利用隨機實例選擇線程要首先鎖定的資源。
假設布爾變量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
請求鎖的順序始終是一致的。兩個線程都會以相同的順序請求鎖。那麽最多會臨時堵塞一個線程,終於都能夠順序運行。
大概來說,造成死鎖須要例如以下的一些條件:
- 相互排斥:必須存在一個資源在某個時刻,僅能由一個線程訪問。
- 資源持有:當鎖定了一個資源的時候。線程仍然須要去獲得另外一個資源的鎖。
- 沒有搶占策略:當某個線程已經持有了資源一段時間的時候。沒有能夠強占線程鎖定資源的機制。
- 循環等待:在運行時必須存在兩個或者很多其它的線程。相互請求對方鎖定的資源。
雖然產生死鎖的條件看起來較多,可是在多線程應用中存在死鎖還是比較常見的。
開發人員能夠通過打破死鎖構成的必要條件來避免死鎖的產生,參考例如以下:
- 相互排斥: 這個需求通常來說是不可避免的。資源非常多時候確實僅僅能相互排斥訪問的。可是並非總是這樣的。
當使用DBMS系統的時候,可能使用相似樂觀鎖的方式來取代原來的悲觀鎖的機制(在更新數據的時候鎖定表中的一行)。
- 還有一種可行的方案,就是對資源持有進行處理,當獲取了某一資源的鎖之後。立馬獲取其它所必須資源的鎖。假設獲取鎖失敗了。則釋放掉之前全部的相互排斥資源。
當然,這樣的方式並非總是能夠的。有可能鎖定的資源之前是無法知道的,或者是廢棄了的資源。
- 假設鎖不能立馬獲取,防止出現死鎖的一種方式就是給鎖的獲取配置上一個超時時間。在SDK類中的
ReentrantLock
就提供了相似超時的方法。 - 從上面的代碼中,我們能夠發現,假設每一個線程的鎖定資源的順序是相同的,是不會產生死鎖的。而這個過程能夠通過將全部請求鎖的代碼都抽象到一個方法。然後由線程調用來實現。這就能夠有效的避免死鎖。
在一個更高級的應用中,開發人員也許須要考慮實現一個檢測死鎖的系統。
在這個系統中,來實現一些基於線程的監控,當前程獲取一個鎖。而且嘗試請求別的鎖的時候。都記錄日誌。假設以線程和鎖構成有向圖。開發人員是能夠檢測到2不同的線程持有資源而且同一時候請求另外的堵塞的資源的。假設開發人員能夠檢測。並能夠強制堵塞的線程釋放掉已經獲取的資源,就能夠自己主動檢測到死鎖而且自己主動修復死鎖問題。
饑餓
線程調度器會決定哪一個處於RUNNABLE
狀態的線程會的運行順序。決定通常是基於線程的優先級的;因此,低優先級的線程會獲得較少的CPU時間,而高優先級的線程會獲得較多的CPU時間。當然,這樣的調度聽起來較為合理。可是有的時候也會引起問題。假設總是運行高優先級的線程,那麽低優先級的線程就會無法獲得足夠的時間來運行,處於一種饑餓狀態。
因此。建議開發人員僅僅在真的十分必要的時候才去配置線程的優先級。
一個非常復雜的線程饑餓的樣例就是finalize()
方法。Java語言中的這一特性能夠用來進行垃圾回收。可是當開發人員查看一下finalizer
線程的優先級。就會發現其運行的優先級不是最高的。因此,非常有可能finalize()
方法跟其它方法比起來會運行更久。
還有一個運行時間的問題是。線程以何種順序通過同步代碼塊是未定義的。
當非常多並行線程須要通過封裝的同步代碼塊時,會有的線程等待的時間要比其它線程的時間更久才幹進入同步代碼快。理論上,他們可能永遠無法進入代碼塊。這個問題能夠使用公平鎖的方案來解決。
公平鎖在選擇下個線程的時候會考慮到線程的等待時間。當中一個公平鎖的實現就是java.util.concurrent.locks.ReentrantLock
:
假設使用ReentrantLock
的例如以下構造函數:
/**
* Creates an instance of [email protected] ReentrantLock} with the
* given fairness policy.
*
* @param fair [email protected] true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
傳入true
,那麽ReentrantLock
是一個公平鎖,是會同意線程按掛起順序來依次獲取鎖運行的。
這樣能夠削減線程的饑餓,可是,並不能全然解決饑餓的問題,畢竟線程的調度是由操作系統調度的。所以。ReentrantLock
類僅僅考慮等待鎖的線程,調度上是無法起作用的。舉個樣例。雖然使用了公平鎖,可是操作系統會給低優先級的線程非常短的運行時間。
Java線程和多線程(十五)——線程的活性