Java 5
初次引入了Concurrency API,並在隨後的釋出版本中不斷優化和改進。這篇文章的大部分概念也適用於老的版本。我的程式碼示例主要聚焦在Java 8
上,並大量適用 lambda
表示式和一些新特性。如果你還不熟悉 lambda
表示式,建議先閱讀 Java 8 Tutorial。
Threads
和 Runnables
所有現代作業系統都是通過程式
和執行緒
來支援併發的。程式
通常是相互獨立執行的程式例項。例如,你啟動一個 Java
程式,作業系統會產生一個新的程式
和其他程式並行執行。在這些程式
中可以利用執行緒
同時執行程式碼。這樣我們就可以充分利用 CPU
。
Java
從 JDK 1.0
開始就支援執行緒
。在開始一個新執行緒
之前,必須先指定執行的程式碼,通常稱為 Task
。下面是通過實現 Runnable
介面來啟動一個新執行緒的例子:
Runnable task = () -> {
String threadName = Thread.currentThread().getName();
System.out.println("Hello " + threadName);
};
task.run();
Thread thread = new Thread(task);
thread.start();
System.out.println("Done!");
複製程式碼
由於 Runnable
是一個函式式
介面,我們可以使用 lambda
表示式來列印執行緒的名字到控制檯。我們直接在主執行緒上執行Runnable
,然後開始一個新執行緒。在控制檯你將看到這樣的結果:
Hello main
Hello Thread-0
Done!
複製程式碼
或者:
Hello main
Done!
Hello Thread-0
複製程式碼
由於是併發
執行,我們無法預測 Runnable
是在列印 Done
之前還是之後呼叫,順序不是不確定的,因此併發程式設計
成為大型應用程式開發中一項複雜的任務。
執行緒也可以休眠一段時間,例如下面的例子:
Runnable runnable = () -> {
try {
String name = Thread.currentThread().getName();
System.out.println("Foo " + name);
TimeUnit.SECONDS.sleep(1);
System.out.println("Bar " + name);
}
catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
複製程式碼
執行上面的程式碼會在兩個列印語句之間停留1秒鐘。TimeUnit
是一個時間單位的列舉,或者可以通過呼叫 Thread.sleep(1000)
實現。
使用 Thread
類可能非常繁瑣且容易出錯。由於這個原因,在2004年,Java 5
版本引入了 Concurrency API
。API
位於 java.util.concurrent
包下,包含了許多有用的有關併發程式設計的類。從那時起,每個新發布的 Java
版本都增加了併發 API
,Java 8
也提供了新的類和方法來處理併發。
現在我們來深入瞭解一下Concurrency API
中最重要的部分 - executor services
。
Executors
Concurrency API
引入了 ExecutorService
的概念,作為處理執行緒的高階別方式用來替代 Threads
。 Executors
能夠非同步的執行任務,並且通常管理一個執行緒池。這樣我們就不用手動的去建立執行緒了,執行緒池中的所有執行緒都將被重用。從而可以在一個
executor service
的整個應用程式生命週期中執行儘可能多的併發任務。
下面是一個簡單的 executors
例子:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Hello " + threadName);
});
// => Hello pool-1-thread-1
複製程式碼
Executors
類提供了方便的工廠方法來建立不同型別的 executor services
。在這個例子中使用了只執行一個執行緒的 executor
。
執行結果看起來和上面的示例類似,但是你會注意到一個重要區別:Java
程式永遠不會停止,執行者必須明確的停止它,否則它會不斷的接受新的任務。
ExecutorService
為此提供了兩種方法:shutdown()
等待當前任務執行完畢,而 shutdownNow()
則中斷所有正在執行的任務,並立即關閉執行程式。在 shudown
之後不能再提交任務到執行緒池。
下面是我關閉程式的首選方式:
try {
System.out.println("attempt to shutdown executor");
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
System.err.println("tasks interrupted");
}
finally {
if (!executor.isTerminated()) {
System.err.println("cancel non-finished tasks");
}
executor.shutdownNow();
System.out.println("shutdown finished");
}
複製程式碼
執行者呼叫 shutdown
關閉 executor
,在等待 5 秒鐘鍾後,不管任務有沒有執行完畢都呼叫 shutdownNow
中斷正在執行的任務而關閉。
Callables 和 Futures
除了 Runnable
以外,executors
還支援 Callable
任務,和 Runnable
一樣是一個函式式介面,但它是有返回值的。
下面是一個使用 lambda
表示式定義的 Callable
,在睡眠 1 秒後返回一個整形值。
Callable<Integer> task = () -> {
try {
TimeUnit.SECONDS.sleep(1);
return 123;
}
catch (InterruptedException e) {
throw new IllegalStateException("task interrupted", e);
}
};
複製程式碼
和 Runnable
一樣,Callable
也可以提交到 executor services
,但是執行的結果是什麼?由於 submit()
不等待任務執行完成,executor service
不能直接返回撥用的結果。相對應的,它返回一個 Future
型別的結果,使用 Future
可以檢索實際執行結果。
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(task);
System.out.println("future done? " + future.isDone());
Integer result = future.get();
System.out.println("future done? " + future.isDone());
System.out.print("result: " + result);
複製程式碼
在將 Callable
提交給 executor
後,首先通過 isDone()
來檢查 future
是否執行完畢。我敢肯定,情況並非如此,因為上面的呼叫在返回整數之前睡眠了 1 秒鐘。
呼叫方法 get()
會阻塞當前執行緒,直到 callable
執行完成返回結果,現在 future
執行完成,並在控制檯輸出下面的結果:
future done? false
future done? true
result: 123
複製程式碼
Future
與 executor service
緊密結合,如果關閉 executor service
, 每個 Future
都會丟擲異常。
executor.shutdownNow();
future.get();
複製程式碼
這裡建立 executor
的方式與前面的示例不同,這裡使用 newFixedThreadPool(1)
來建立一個執行緒數量為 1 的執行緒池來支援 executor
, 這相當於 newSingleThreadExecutor()
,稍後我們我們會通過傳遞一個大於 1 的值來增加執行緒池的大小。
Timeouts
任何對 future.get()
的呼叫都會阻塞並等待 Callable
被終止。 在最壞的情況下,一個可呼叫函式將永遠執行,從而使應用程式無法響應。可以簡單地通過超時來抵消這些情況:
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(() -> {
try {
TimeUnit.SECONDS.sleep(2);
return 123;
}
catch (InterruptedException e) {
throw new IllegalStateException("task interrupted", e);
}
});
future.get(1, TimeUnit.SECONDS);
複製程式碼
執行上面的程式碼會丟擲 TimeoutException
Exception in thread "main" java.util.concurrent.TimeoutException
at java.util.concurrent.FutureTask.get(FutureTask.java:205)
複製程式碼
指定了 1 秒鐘的最長等待時間,但是在返回結果之前,可呼叫事實上需要 2 秒鐘的時間。
InvokeAll
Executors
支援通過 invokeAll()
批量提交多個 Callable
。這個方法接受一個 Callable
型別集合的引數,並返回一個 Future
型別的 List
。
ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<String>> callables = Arrays.asList(
() -> "task1",
() -> "task2",
() -> "task3");
executor.invokeAll(callables)
.stream()
.map(future -> {
try {
return future.get();
}
catch (Exception e) {
throw new IllegalStateException(e);
}
})
.forEach(System.out::println);
複製程式碼
在這個例子中,我們利用 Java 8
的流來處理 invokeAll
呼叫返回的所有 Future
。 我們首先對映每個 Future
的返回值,然後將每個值列印到控制檯。 如果還不熟悉流,請閱讀Java 8 Stream Tutorial。
InvokeAny
批量提交可呼叫的另一種方法是 invokeAny()
,它與 invokeAll()
略有不同。 該方法不會返回所有的 Future
物件,它只返回第一個執行完畢任務的結果。
Callable<String> callable(String result, long sleepSeconds) {
return () -> {
TimeUnit.SECONDS.sleep(sleepSeconds);
return result;
};
}
複製程式碼
我們使用這種方法來建立一個有三個不同睡眠時間的 Callable
。 通過 invokeAny()
將這些可呼叫物件提交給 executor
,返回最快執行完畢結果,在這種情況下,task2:
ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<String>> callables = Arrays.asList(
callable("task1", 2),
callable("task2", 1),
callable("task3", 3));
String result = executor.invokeAny(callables);
System.out.println(result);
// => task2
複製程式碼
上面的例子使用通過 newWorkStealingPool()
建立的另一種型別的 executor
。 這個工廠方法是 Java 8
的一部分,並且返回一個型別為 ForkJoinPool
的 executor
,它與正常的 executor
略有不同。 它不使用固定大小的執行緒池,預設情況下是主機CPU的可用核心數。
Scheduled Executors
我們已經學會了如何在 Executors
上提交和執行任務。 為了多次定期執行任務,我們可以使用 scheduled thread pools
。
ScheduledExecutorService
能夠安排任務定期執行或在一段時間過後執行一次。
下面程式碼示例一個任務在三秒鐘後執行:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());
ScheduledFuture<?> future = executor.schedule(task, 3, TimeUnit.SECONDS);
TimeUnit.MILLISECONDS.sleep(1337);
long remainingDelay = future.getDelay(TimeUnit.MILLISECONDS);
System.out.printf("Remaining Delay: %sms", remainingDelay);
複製程式碼
排程任務產生一個型別為 ScheduledFuture
的值,除了 Future
之外,它還提供getDelay()
方法來檢索任務執行的剩餘時間。
為了定時執行的任務,executor
提供了兩個方法 scheduleAtFixedRate()
和
scheduleWithFixedDelay()
。 第一種方法能夠執行具有固定時間間隔的任務,例如, 每秒一次:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());
int initialDelay = 0;
int period = 1;
executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);
複製程式碼
此外,此方法還可以設定延遲時間,該延遲描述了首次執行任務之前的等待時間。
scheduleWithFixedDelay()
方法與 scheduleAtFixedRate()
略有不同,不同之處是它們的等待時間,scheduleWithFixedDelay()
的等待時間是在上一個任務結束和下一個任務開始之間施加的。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("Scheduling: " + System.nanoTime());
}
catch (InterruptedException e) {
System.err.println("task interrupted");
}
};
executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);
複製程式碼
本示例在執行結束和下一次執行開始之間延遲 1 秒。 初始延遲為 0,任務持續時間為 2 秒。 所以我們結束了一個0s,3s,6s,9s等的執行間隔。