本部落格是“Java 多線程程式設計”系列的後續篇。“Java 多線程程式設計”系列其他部落格請參閱本部落格結尾部分。
有多個線程,如何控制它們執行的先後次序?
方法一:
設定線程優先級。
java.lang.Thread 提供了 setPriority(int newPriority) 方法來設定線程的優先級,但線程的優先級是無法保障線程的執行次序的,優先級隻是提高了優先級高的線程擷取 CPU 資源的機率。也就是說,這個方法不靠譜。
方法二:
使用線程合并。
使用 java.lang.Thread 的 join() 方法。比如有線程 a,現在目前線程想等待 a 執行完之後再往下執行,那就可以使用 a.join()。一旦線程使用了 a.join(),那麼目前線程會一直等待 a 消亡之後才會繼續執行。什麼時候 a 消亡?a 的 run() 方法執行結束了 a 就消亡了。
這個方法可以有效地進行線程排程,但卻隻能局限于等待一個線程的執行排程。如果要等待 N 個線程的話,顯然是無能為力了。而且等待線程必須在被等待線程消亡後才得到繼續執行的指令,無法做到兩個線程真正意義上的并發,靈活性較差。
方法三:
使用線程通信。
java.lang.Object 提供了可以進行線程間通信的 wait 與 notify 、notifyAll 等方法。每個 Java 對象都有一個隐性的線程鎖的概念,通過這個線程鎖的概念我們讓線程間可以進行通信,各線程不再埋頭單幹。著名的“生産者-消費者”模型就是基于這個原理實作的。
這個方法也可以有效地進行線程排程,而且也不僅僅局限于等待一個線程的執行排程,具有很大程度上的靈活性。但操作複雜,不易控制容易造成混亂,程式維護起來也不太友善。
方法四:
使用閉鎖。
閉鎖就像一扇門,在先決條件未達成之前這扇門是閉着的,線程無法通過,先決條件達成之後,閉鎖打開,線程就可以繼續執行了。java.util.concurrent.CountDownLatch 是一個很實用的閉鎖實作,它提供了 countDown() 和 await() 方法達成線程執行隊列,這個方法最适合 M 個線程等待 N 個線程執行結束再執行的情況。首先初始化一個 CountDownLatch 對象,比如 CountDownLatch doneSignal = new CountDownLatch(N);該對象具有 N 作為計數閥值,每個被等待線程通過對 doneSignal 對象的持有,使用 countDown() 可以将 doneSignal 的計數閥值減一;每個等待線程通過對 doneSignal 對象的持有,使用 await() 阻塞目前線程,直到 doneSignal 計數閥值減為 0,才繼續往下執行。
這個方法也可以有效地進行線程排程,而且比方法三更易于管理,開發者隻需控制好 CountDownLatch 即可。但線程執行次序管理相對單一,它隻是指出目前等待線程的數量,而且 CountDownLatch 的初始閥值一旦設定就隻能遞減下去,無法重置。如需遞減過程中進行閥值的重置可以參考 java.util.concurrent.CyclicBarrier。
不管如何,CountDownLatch 對于一定條件下的線程隊列的達成還是很有用的。對于複雜環境下的線程管理還是卓有成效的。是以熟悉和把握對它的使用還是很有必要的。
以下是一個實際項目中 CountDownLatch 的使用的例子:
private Map<Long,DecryptSignalAndPath> afterDecryptFilePathMap = new HashMap<Long,DecryptSignalAndPath>();//TODO 注意容器垃圾資料的清理工作
class DecryptRunnable implements Runnable {
private ServerFileBean serverFile;
private Long fid;//指向解密檔案
private CountDownLatch decryptSignal;
protected DecryptRunnable(Long fid, ServerFileBean serverFile, CountDownLatch decryptSignal) {
this.fid = fid;
this.serverFile = serverFile;
this.decryptSignal = decryptSignal;
}
@Override
public void run() {
//開始解密
String afterDecryptFilePath = null;
DecryptSignalAndPath decryptSignalAndPath = new DecryptSignalAndPath();
decryptSignalAndPath.setDecryptSignal(decryptSignal);
afterDecryptFilePathMap.put(fid, decryptSignalAndPath);
afterDecryptFilePath = decryptFile(serverFile);
decryptSignalAndPath.setAfterDecryptFilePath(afterDecryptFilePath);
decryptSignal.countDown();//通知所有阻塞的線程
}
}
class DecryptSignalAndPath {
private String afterDecryptFilePath;
private CountDownLatch decryptSignal;
public String getAfterDecryptFilePath() {
return afterDecryptFilePath;
}
public void setAfterDecryptFilePath(String afterDecryptFilePath) {
this.afterDecryptFilePath = afterDecryptFilePath;
}
public CountDownLatch getDecryptSignal() {
return decryptSignal;
}
public void setDecryptSignal(CountDownLatch decryptSignal) {
this.decryptSignal = decryptSignal;
}
}
需要先執行的,被等待線程在這裡加入:
CountDownLatch decryptSignal = new CountDownLatch(1);
new Thread(new DecryptRunnable(fid, serverFile, decryptSignal)).start();//無需拿到新線程句柄,由 CountDownLatch 自行跟蹤
try {
decryptSignal.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
}
需要後執行,等待的線程可以這樣加入:
CountDownLatch decryptSignal = afterDecryptFilePathMap.get(fid).getDecryptSignal();
try {
decryptSignal.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
}
當然,這也僅僅隻是一個簡單的 CountDownLatch 的使用展示,對于 CountDownLatch 來說有點大材小用了,因為它可以勝任更複雜的多線程環境。示例中的案例完全可以使用線程通信進行搞定。因為 CountDownLatch 的閥值初始為 1,是以這裡甚至完全可以使用方法二所說的線程的合并進行取代。
class Driver2 { // ...
void main() throws InterruptedException {
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = ...
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
doneSignal.await(); // wait for all to finish
}
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
try {
doWork(i);
doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
}
void doWork() { ... }
}