ForkJoinPool
是 Java SE 7 新功能“分叉/結(jié)合框架”的核心類,現(xiàn)在可能乏人問津,但我覺得它遲早會成為主流。分叉/結(jié)合框架是一個(gè)比較特殊的線程池框架,專用于需要將一個(gè)任務(wù)不斷分解成子任務(wù)(分叉),再不斷進(jìn)行匯總得到最終結(jié)果(結(jié)合)的計(jì)算過程。比起傳統(tǒng)的線程池類 ThreadPoolExecutor
,ForkJoinPool
實(shí)現(xiàn)了工作竊取算法,使得空閑線程能夠主動分擔(dān)從別的線程分解出來的子任務(wù),從而讓所有的線程都盡可能處于飽滿的工作狀態(tài),提高執(zhí)行效率。
ForkJoinPool
提供了三類方法來調(diào)度子任務(wù):
execute
系列
- 異步執(zhí)行指定的任務(wù)。
invoke
和 invokeAll
- 執(zhí)行指定的任務(wù),等待完成,返回結(jié)果。
submit
系列
- 異步執(zhí)行指定的任務(wù)并立即返回一個(gè)
Future
對象。
子任務(wù)由 ForkJoinTask
的實(shí)例來代表。它是一個(gè)抽象類,JDK 為我們提供了兩個(gè)實(shí)現(xiàn):RecursiveTask
和 RecursiveAction
,分別用于需要和不需要返回計(jì)算結(jié)果的子任務(wù)。ForkJoinTask
提供了三個(gè)靜態(tài)的 invokeAll
方法來調(diào)度子任務(wù),注意只能在 ForkJoinPool
執(zhí)行計(jì)算的過程中調(diào)用它們。
ForkJoinPool
和 ForkJoinTask
還提供了很多讓人眼花繚亂的公共方法,其實(shí)它們大多數(shù)都是其內(nèi)部實(shí)現(xiàn)去調(diào)用的,對于應(yīng)用開發(fā)人員來說意義不大。
下面以統(tǒng)計(jì) D 盤文件個(gè)數(shù)為例。這實(shí)際上是對一個(gè)文件樹的遍歷,我們需要遞歸地統(tǒng)計(jì)每個(gè)目錄下的文件數(shù)量,最后匯總,非常適合用分叉/結(jié)合框架來處理:
// 處理單個(gè)目錄的任務(wù)
public class CountingTask extends RecursiveTask<Integer> {
private Path dir;
public CountingTask(Path dir) {
this.dir = dir;
}
@Override
protected Integer compute() {
int count = 0;
List<CountingTask> subTasks = new ArrayList<>();
// 讀取目錄 dir 的子路徑。
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
for (Path subPath : ds) {
if (Files.isDirectory(subPath, LinkOption.NOFOLLOW_LINKS)) {
// 對每個(gè)子目錄都新建一個(gè)子任務(wù)。
subTasks.add(new CountingTask(subPath));
} else {
// 遇到文件,則計(jì)數(shù)器增加 1。
count++;
}
}
if (!subTasks.isEmpty()) {
// 在當(dāng)前的 ForkJoinPool 上調(diào)度所有的子任務(wù)。
for (CountingTask subTask : invokeAll(subTasks)) {
count += subTask.join();
}
}
} catch (IOException ex) {
return 0;
}
return count;
}
}
// 用一個(gè) ForkJoinPool 實(shí)例調(diào)度“總?cè)蝿?wù)”,然后敬請期待結(jié)果……
Integer count = new ForkJoinPool().invoke(new CountingTask(Paths.get("D:/")));
在我的筆記本上,經(jīng)多次運(yùn)行這段代碼,耗費(fèi)的時(shí)間穩(wěn)定在 600 豪秒左右。普通線程池(Executors.newCachedThreadPool()
)耗時(shí) 1100 毫秒左右,足見工作竊取的優(yōu)勢。
結(jié)束本文前,我們來圍觀一個(gè)最神奇的結(jié)果:單線程算法(使用 Files.walkFileTree(...)
)比這兩個(gè)都快,平均耗時(shí) 550 毫秒!這警告我們并非引入多線程就能優(yōu)化性能,并須要先經(jīng)過多次測試才能下結(jié)論。