本篇文章通過服務器通信
和頁面渲染
兩個功能的實現來加深多線程中Future
和Executor
的理解。
服務器通信
串行執行任務
任務執行最簡單的策略就是在單線程中串行執行各項任務,並不會涉及多線程。
以創建通訊服務爲例,我們可以這樣實現(很low)
@Test
public void singleThread() throws IOException {
ServerSocket serverSocket= new ServerSocket(8088);
while (true){
Socket conn = serverSocket.accept();
handleRequest(conn);
}
}
代碼很簡單,理論上沒什麼毛病,但是實際使用中只能處理一個請求。但是當處理任務很耗時並且在多次請求時會阻塞無法及時響應。
由此可見串行處理機制通常都無法提供高吞吐率或快速響應性。
顯式的爲任務創建線程
串行執行任務這麼 low,我們來通過多線程來處理請求吧:當接收到請求後創建新的線程去執行任務。new Thread()應該就能實現。
初級版本:
@Test
public void perThreadTask() throws IOException {
ServerSocket serverSocket = new ServerSocket(8088);
while (true) {
Socket conn = serverSocket.accept();
Runnable r = new Runnable() {
@Override
public void run() {
handleRequest(conn);
}
};
new Thread(r).start();
}
}
微弱的優點
- 對於每個請求,都創建了一個線程來處理,達到多線程並行效果
- 任務處理從主線程分離出來,使得主循環能更快的處理下一個請求
爲每個任務分配一個線程存在一些缺陷,尤其當需要創建大量的線程時
- 線程生命週期的開銷非常高。根據平臺的不同,實際的開銷也不同。但是線程的創建過程都會需要時間,並且需要 JVM 和操作系統提供一些輔助操作。
- 資源消耗。活躍的線程會消耗系統資源,尤其是內存。如果可運行的線程數量多餘可用處理器的數量,那麼有些線程將閒置。大量閒置的線程會佔用許多內存,給垃圾回收器帶來壓力。如果你已經擁有足夠多的線程使所有 CPU 保持忙碌狀態,那麼多餘的線程反而會降低性能。
- 穩定性。隨着平臺的不同,可創建線程數量的限制是不同的,並受多個因素制約,包括 JVM 的啓動參數、Thread 構造函數中請求的棧大小,以及底層操作系統對線程的限制等。如果破壞了這些限制,很可能拋出 OOM 異常。
上面兩種方式都存在一些問題:單線程串行的問題在於其糟糕的響應性和吞吐量;而爲每個任務分配線程的問題在於資源消耗和管理的複雜性。
在 Java 類庫中,任務執行的主要抽象不是 Thread,而是 Executor
public interface Executor {
void execute(Runnable command);
}
Executor 框架
Executor 基於生產者-消費者模式,提交任務的操作相當於生產者,執行任務的線程相當於消費者。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HTHOWF5T-1577951875761)(http://www.hualee.top/upload/2019/12/0l7kl5evqahber5ol8paosfp4m.png)]
通訊優化
對於以前的通訊服務我們可以用 Executor
進一步優化一下
@Test
public void limitExecutorTask() throws IOException {
final int nThreads = 100;
ExecutorService exec = Executors.newFixedThreadPool(nThreads);
ServerSocket serverSocket = new ServerSocket(8088);
while (true) {
Socket conn = serverSocket.accept();
Runnable r = new Runnable() {
@Override
public void run() {
handleRequest(conn);
}
};
exec.execute(r);
}
}
線程池
線程池從字面來看時指管理一組同構工作線程的資源池。它與工作隊列密切相關,它在工作隊列中保存了所有等待執行的任務。
線程池通過重用現有的線程而不是創建新線程,可以在處理多個請求時分攤在線程創建和銷燬過程中產生的巨大開銷。另一個額外的好處是,當請求到達時,工作線程已經存在,因此不會由於等待創建線程而延遲任務的執行,挺高響應性。
JAVA 類庫中提供了一個靈活的線程池以及一些有用的默認配置。可以通過 Executors 中的靜態工廠方法來創建。
-
newFixedThreadPool
將創建一個固定長度的線程池,每當提交一個任務時就創建一個線程,直到達到線程的最大數量。 -
newCacheedThreadPool
將創建一個可緩存的線程池,如果線程池的當前規模超過了處理需求時,那麼將回收空閒的線程,而當需求增加時,則可以添加新的線程,線程池的規模則不存在限制。 -
newSingleThreadPool
是一個單線程的 Executor,它創建單個工作者線程來執行任務,如果這個線程異常結束,會創建另一個線程來替代。newSingleThreadPool
能確保依照任務在隊列中的順序來串行執行。 -
newScheduledThreadPool
創建一個固定長度的線程池,而且以延遲或定時的方式來執行任務,類似於 Timer。
Executor 生命週期
爲了解決執行服務的聲明週期問題,Executor
擴展了 ExecutorService
接口,添加了一些用於管理生命週期的方法shutdown()
,shutdownNow()
,isShutdown()
,isTerminated()
,awaitTermination()
。
ExecutorService
的生命週期有3中狀態:運行、關閉和已終止。初始創建時處於運行狀態。
shutdown()
方法將執行平緩的關閉過程:不再接受新的任務,同時等待已經提交的任務執行完成,包括那些還未開始執行的任務。shutdownNow()
方法將執行粗暴的關閉過程:它將嘗試取消所有運行中任務,並且不再啓動隊列中尚未開始執行的任務。
等待所有任務完成後,ExecutorService
將轉入終止狀態。可以調用awaitTermination
來等待到達終止狀態,或者通過isTerminated
來輪詢是否已終止。
服務器通訊初步牛批版本
class LifecycleWebServer {
private ExecutorService exec;
public void start() throws IOException {
ServerSocket socket = new ServerSocket(80);
while (!exec.isShutdown()) {
try {
Socket conn = socket.accept();
exec.execute(new Runnable() {
@Override
public void run() {
handleRequest(conn);
}
});
}catch (RejectedExecutionException e){
if (!exec.isShutdown()){
System.out.println("task submission reject::"+e);
}
}
}
}
public void stop(){
exec.shutdown();
}
void handleRequest(Socket conn) {
Request req = readRequest(conn);
if(isShutdownRequest(req)){
stop();
}else {
dispatchRequest(req);
}
}
private void dispatchRequest(Request req) {
//......分發請求
}
private boolean isShutdownRequest(Request req) {
//......判斷是否是 shutdown 請求
}
private Request readRequest(Socket conn) {
//......解析請求
}
}
通過 ExecutorService
增加對任務生命週期的管理。
延遲任務與生命週期
Timer
是作者使用較多的任務類,主要用來管理延遲任務以及週期任務。因爲 Timer
本身還是存在一些缺陷:
Timer
在執行所有定時任務時只會創建一個線程。如果某個任務的執行時間過長,那麼將破壞其他TimerTask
的定時精確性。public void timerTest() { Timer timer = new Timer(); System.out.println("Timer Test Start " +new Date()); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("001 working current " +new Date()); try { Thread.sleep(4*1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("001 working current " +new Date()); } },1000); timer.schedule(new TimerTask() { @Override public void run() { try { Thread.sleep(1000); System.out.println("002 working current " +new Date()); Thread.sleep(1000); System.out.println("002 working current " +new Date()); Thread.sleep(1000); System.out.println("002 working current " +new Date()); Thread.sleep(1000); System.out.println("002 working current " +new Date()); } catch (InterruptedException e) { e.printStackTrace(); } } },2000); }
打印 log:
Timer Test Start Tue Dec 10 11:52:44 CST 2019
001 working current Tue Dec 10 11:52:45 CST 2019
001 working current Tue Dec 10 11:52:49 CST 2019
002 working current Tue Dec 10 11:52:50 CST 2019
002 working current Tue Dec 10 11:52:51 CST 2019
002 working current Tue Dec 10 11:52:52 CST 2019
002 working current Tue Dec 10 11:52:53 CST 2019
從時間戳上可以看出兩個 TimerTask 是串行執行的。時間調度出現了問題
- 另一個是線程泄露問題:當 TimerTask 拋出一個未檢查的異常,那麼 Timer 將表現出糟糕的行爲。Timer 線程並不捕獲異常,因此當 TimerTask 拋出未檢查的異常時將終止定時線程,並且不會恢復線程的執行。
請儘量減少或者停止 Timer 的使用,ScheduledThreadPoolExecutor
能夠正確處理這些表現出錯誤行爲的任務。
public void testScheduled(){
ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(10);
System.out.println("scheduled test " + new Date());
ScheduledFuture<?> work1 = executor.schedule(new Callable<String>() {
@Override
public String call() throws Exception {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("001 Worker " + new Date());
return "work1 finish";
}
}, 1, TimeUnit.SECONDS);
ScheduledFuture<?> work2 = executor.schedule(new Callable<String>() {
@Override
public String call() throws Exception {
try {
Thread.sleep(1000);
System.out.println("002 Worker " + new Date());
Thread.sleep(1000);
System.out.println("002 Worker " + new Date());
Thread.sleep(1000);
System.out.println("002 Worker " + new Date());
Thread.sleep(1000);
System.out.println("002 Worker " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
return "work2 Finish";
}
}, 2, TimeUnit.SECONDS);
}
輸出 log:
scheduled test Tue Dec 10 15:54:10 CST 2019
002 Worker Tue Dec 10 15:54:13 CST 2019
002 Worker Tue Dec 10 15:54:14 CST 2019
001 Worker Tue Dec 10 15:54:15 CST 2019
002 Worker Tue Dec 10 15:54:15 CST 2019
002 Worker Tue Dec 10 15:54:16 CST 2019
從 log 來看,時間調度上符合我們的預期,棒棒噠。
頁面渲染
來自面試官的提問:瀏覽器是怎樣加載網頁的?
方法一:使用簡單串行
最簡單的方法是對HTML文檔進行串行處理。當遇到文本標籤時,將其繪製到圖像緩存中。當遇到圖像引用時,先通過網絡獲取,然後再將其繪製到圖像緩存中。這種方式算是一種思路,但是可能會令使用者感到方案,他們必須等待很長時間,直到顯示所有的文本。
@Test
public void singleThreadRender() {
CharSequence source = "";
renderText(source);
List<ImageData> imageDatas = new ArrayList<>();
for (ImageInfo imageInfo : scanForImageInfo(source)) {
imageDatas.add(imageInfo.downloadImage());
}
for (ImageData imageData : imageDatas) {
renderImage(imageData);
}
}
瞭解 Callable
和 Future
Executor
框架使用 Runnable
作爲其基本的任務表示形式。Runnable
是一種有很大侷限的抽象,雖然能夠異步執行任務,但是它不能返回一個值或者拋出受檢查的異常。
許多任務實際上都是存在延遲的計算(像執行數據庫查詢、從網絡上獲取資源、或者計算某個複雜的功能)。對於這些任務,Callable
是一種更好的抽象:它認爲主入口點應該返回一個值,並可能拋出一個異常。
Runnable
和Callable
描述的都是抽象的計算任務。這些任務通常都應該有一個明確的起始點,並且最終會結束。Executor
執行任務有4個生命週期階段:創建、提交、開始和完成。由於有些任務可能需要很長的時間,因此通常希望能夠及時取消。再 Executor
框架中,已提交但尚未開始的任務可以取消,但是對於那些已經開始的任務,只有當它們能響應中斷時,才能取消。
Future
表示一個任務的生命週期,並提供了相應的方法來判斷任務是否已經完成或取消,以及獲取任務的結果和取消任務等。在 Future
規範中包含的隱含意義是,任務的聲明週期只能前進,不能後腿,就像ExcutorService
的生命週期一樣。當某個任務完成後,它就永遠停留在完成
狀態上。
Future 包含如下方法:
interface Future{
boolean cancel()
boolean get()
boolean isCancelled()
boolean isDone()
}
get()
方法的行爲取決於任務的狀態(尚未開始、正在運行、已完成)。如果任務已完成,方法會立即返回或者拋出一個異常;如果任務沒有完成,方法 將阻塞直到任務完成。
可以通過多種方法創建一個Future
來描述任務。ExecutorService
中的所有的 submit 方法都將返回一個Future
,從而將一個Runnable
或者Callable
提交給 Executor
,並得到一個 Future
用來獲取任務的執行結果或者取消任務。
方法二:使用Future
實現渲染
爲了使頁面渲染具有更高的併發性,我們分解成兩個任務:一個是渲染所有的文本(
CPU 密集型
);另一個是下載所有的圖像(I/O 密集型
)。
Callable
和 Future
有助於協同任務之間的交互。
@Test
public void futureRender() {
CharSequence source = "";
ExecutorService executor = Executors.newFixedThreadPool(10);
List<ImageInfo> imageInfos = scanForImageInfo(source);
Callable<List<ImageData>> task = new Callable<List<ImageData>>() {
@Override
public List<ImageData> call() throws Exception {
List<ImageData> result = new ArrayList<>();
for (ImageInfo imageInfo : imageInfos) {
result.add(imageInfo.downloadImage());
}
return result;
}
};
Future<List<ImageData>> future = executor.submit(task);
renderText(source);
try {
List<ImageData> imageDatas = future.get();
for (ImageData imageData : imageDatas) {
renderImage(imageData);
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
future.cancel(true);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
futureRender
使得渲染文本與下載圖像數據的任務併發執行,當所有圖像下載完成後,會顯示到頁面上。對比串行版本已經提高了效率和用戶體驗。但我們還可以做得更好,我們不必等到所有的圖像都下載完成,而是希望沒下載完一副圖像就顯示出來。
瞭解CompletionService
CompletionService
的實現類是ExecutorCompletionService
,它將Executor
和BlockingQueue
的功能融合在一起。
如果想及時獲取任計算的結果,按照前面的思路我們可以先保留任務提交Executor
後返回的 Future
,然後不斷的調用get()
方法來獲取。這種方式雖然可行,但是不夠優雅。幸運的是有CompletionService
。
請仔細閱讀take()
方法說明:
/**
* Retrieves and removes the Future representing the next
* completed task, waiting if none are yet present.
*
* @return the Future representing the next completed task
* @throws InterruptedException if interrupted while waiting
*/
Future<V> take() throws InterruptedException;
take()
會取出並從隊列移除已完成的任務。so,我們可以這樣實現:
使用CompletionService
實現頁面渲染
@Test
public void completionServiceRender(ExecutorService executor, CharSequence source) {
List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService = new ExecutorCompletionService<>(executor);
for (ImageInfo imageInfo : info) {
completionService.submit(new Callable<ImageData>() {
@Override
public ImageData call() throws Exception {
return imageInfo.downloadImage();
}
});
}
renderText(source);
try {
int taskSize = info.size();
for (int i = 0; i < taskSize; i++) {
Future<ImageData> f = completionService.take();
ImageData data = f.get();
renderImage(data);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
爲任務設置時限
新需求:對於耗時任務,等待特定時間後仍未完成,則取消任務。
需求合情合理。這種情況下,我們可以使用Future
的get()
方法,官方描述如下:
/**
* Waits if necessary for at most the given time for the computation
* to complete, and then retrieves its result, if available.
*
* @param timeout the maximum time to wait
* @param unit the time unit of the timeout argument
* @return the computed result
* @throws CancellationException if the computation was cancelled
* @throws ExecutionException if the computation threw an
* exception
* @throws InterruptedException if the current thread was interrupted
* while waiting
* @throws TimeoutException if the wait timed out
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
- 兩個參數:等待的時間、時間單位。
- 請注意拋出的異常,我們可以通過捕獲
TimeoutException
來處理超時情況。