Java併發編程指南(四):線程執行者

1. 創建一個線程執行者:

使用Executor framework的第一步就是創建一個ThreadPoolExecutor類的對象。你可以使用這個類提供的4個構造器或Executors工廠類來 創建ThreadPoolExecutor。有了執行者,你就可以提交Runnable或Callable對象給執行者來執行。

一個模擬web服務器的示例:

// 1.首先,實現能被服務器執行的任務。創建實現Runnable接口的Task類。
class Task implements Runnable {
    //2.聲明一個類型爲Date,名爲initDate的屬性,來存儲任務創建日期,和一個類型爲String,名爲name的屬性,來存儲任務的名稱。
    private Date initDate;
    private String name;

    // 3.實現Task構造器,初始化這兩個屬性。
    public Task(String name) {
        initDate = new Date();
        this.name = name;
    }

    // 4.實現run()方法。
    @Override
    public void run() {
        // 5.首先,將initDate屬性和實際日期(這是任務的開始日期)寫入到控制檯。
        System.out.printf("%s: Task %s: Created on: %s\n", Thread.currentThread().getName(), name, initDate);
        System.out.printf("%s: Task %s: Started on: %s\n", Thread.currentThread().getName(), name, new Date());
        // 6.然後,使任務睡眠一個隨機時間。
        try {
            Long duration = (long) (Math.random() * 10);
            System.out.printf("%s: Task %s: Doing a task during %dseconds\n", Thread.currentThread().getName(), name, duration);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //7.最後,將任務完成時間寫入控制檯。
        System.out.printf("%s: Task %s: Finished on: %s\n", Thread.currentThread().getName(), name, new Date());
    }
}
// 8.現在,實現服務器類,用來執行使用執行者接受的所有任務。創建一個Server類。
class Server {
    // 9.聲明一個類型爲ThreadPoolExecutor,名爲executor的屬性。
    private ThreadPoolExecutor executor;
    // 10.實現Server構造器,使用Executors類初始化ThreadPoolExecutor對象。
    public Server(){
        executor=(ThreadPoolExecutor)Executors.newCachedThreadPool();
    }
    // 11.實現executeTask()方法,接收Task對象作爲參數並將其提交到執行者。首先,寫入一條信息到控制檯,表明有一個新的任務到達。
    public void executeTask(Task task) {
        System.out.printf("Server: A new task has arrived\n");
        //12.然後,調用執行者的execute()方法來提交這個任務。
        executor.execute(task);
        //13.最後,將執行者的數據寫入到控制檯來看它們的狀態。
        System.out.printf("Server: Pool Size: %d\n", executor.getPoolSize());
        System.out.printf("Server: Active Count: %d\n", executor.getActiveCount());
        System.out.printf("Server: Completed Tasks: %d\n", executor.getCompletedTaskCount());
    }
    //14.實現endServer()方法,在這個方法中,調用執行者的shutdown()方法來結束任務執行。
    public void endServer() {
        executor.shutdown();
    }
}
// 15.最後,實現這個示例的主類,創建Main類,並實現main()方法。
class Main3 {
    public static void main(String[] args) {
        Server server=new Server();
        for (int i=0; i<100; i++){
            Task task=new Task("Task "+i);
            server.executeTask(task);
        }
        server.endServer();
    }
}
Server類是這個示例的關鍵。它創建和使用ThreadPoolExecutor執行任務。
第一個重要點是在Server類的構造器中創建ThreadPoolExecutor。ThreadPoolExecutor有4個不同的構造器,但由於它 們的複雜性,Java併發API提供Executors類來構造執行者和其他相關對象。即使我們可以通過ThreadPoolExecutor類的任意一 個構造器來創建ThreadPoolExecutor,但這裏推薦使用Executors類。
在本例中,你已經使用 newCachedThreadPool()方法創建一個緩存線程池。這個方法返回ExecutorService對象,所以它被轉換爲 ThreadPoolExecutor類型來訪問它的所有方法。你已創建的緩存線程池,當需要執行新的任務會創建新的線程,如果它們已經完成運行任務,變成可用狀態,會重新使用這些線程。線程重複利用的好處是,它減少線程創建的時間。緩存線程池的缺點是,爲新任務不斷創建線程, 所以如果你提交過多的任務給執行者,會使系統超載。
注意事項:使用通過newCachedThreadPool()方法創建的執行者,只有當你有一個合理的線程數或任務有一個很短的執行時間。
一旦你創建執行者,你可以使用execute()方法提交Runnable或Callable類型的任務。在本例中,你提交實現Runnable接口的Task類對象。
你也打印了一些關於執行者信息的日誌信息。特別地,你可以使用了以下方法:
  • getPoolSize():此方法返回線程池實際的線程數。
  • getActiveCount():此方法返回在執行者中正在執行任務的線程數。
  • getCompletedTaskCount():此方法返回執行者完成的任務數。

ThreadPoolExecutor 類和一般執行者的一個關鍵方面是,你必須明確地結束它。如果你沒有這麼做,這個執行者會繼續它的執行,並且這個程序不會結束。如果執行者沒有任務可執行, 它會繼續等待新任務並且不會結束它的執行。一個Java應用程序將不會結束,除非所有的非守護線程完成它們的執行。所以,如果你不結束這個執行者,你的應用程序將不會結束。

當執行者完成所有待處理的任務,你可以使用ThreadPoolExecutor類的shutdown()方法來表明你想要結束執行者。在你調用shutdown()方法之後,如果你試圖提交其他任務給執行者,它將會拒絕,並且拋出RejectedExecutionException異常。

ThreadPoolExecutor 類提供了許多獲取它狀態的方法,我們在這個示例中,使用getPoolSize()、getActiveCount()和 getCompletedTaskCount()方法來獲取執行者的池大小、線程數、完成任務數信息。你也可以使用 getLargestPoolSize()方法,返回池中某一時刻最大的線程數。
ThreadPoolExecutor類也提供其他與結束執行者相關的方法,這些方法是:

  • shutdownNow():此方法立即關閉執行者。它不會執行待處理的任務,但是它會返回待處理任務的列表。當你調用這個方法時,正在運行的任務繼續它們的執行,但這個方法並不會等待它們的結束。
  • isTerminated():如果你已經調用shutdown()或shutdownNow()方法,並且執行者完成關閉它的處理時,此方法返回true。
  • isShutdown():如果你在執行者中調用shutdown()方法,此方法返回true。
  • awaitTermination(long timeout, TimeUnit unit):此方法阻塞調用線程,直到執行者的任務結束或超時。


2. 創建一個大小固定的線程執行者:

當你使用由Executors類的 newCachedThreadPool()方法創建的基本ThreadPoolExecutor,你會有執行者運行在某一時刻的線程數的問題。這個執行者爲每個接收到的任務創建一個線程(如果池中沒有空閒的線程),所以,如果你提交大量的任務,並且它們有很長的(執行)時間,你會使系統過載和引發應用程序性能不佳的問題。

如果你想要避免這個問題,Executors類提供一個方法newFixedThreadPool()來創建大小固定的線程執行者。這個執行者有最大線程數。 如果你提交超過這個最大線程數的任務,這個執行者將不會創建額外的線程,並且剩下的任務將會阻塞,直到執行者有空閒線程。這種行爲,保證執行者不會引發應用程序性能不佳的問題。


3. 執行者執行返回結果的任務:

Executor framework的一個優點是你可以併發執行返回結果的任務。Java併發API使用以下兩種接口來實現:

  • Callable:此接口有一個call()方法。在這個方法中,你必須實現任務的(處理)邏輯。Callable接口是一個參數化的接口。意味着你必須表明call()方法返回的數據類型。
  • Future:此接口有一些方法來保證Callable對象結果的獲取和管理它的狀態。
Future<Integer> result = executor.submit(calculator);


4. 運行多個任務並處理第一個結果:

在併發編程中的一個常見的問題就是,當有多種併發任務解決一個問題時,你只對這些任務的第一個結果感興趣。比如,你想要排序一個數組。你有多種排序算法。 你可以全部啓用它們,並且獲取第一個結果(對於給定數組排序最快的算法的結果)。
String result = executor.invokeAny(taskList);

5.運行多個任務並處理所有結果:

ThreadPoolExecutor類提供一個方法,允許你提交任務列表給執行者,並且在這個列表上等待所有任務的完成。

List<Future<Result>> resultList = executor.invokeAll(taskList);

6. 執行者延遲運行一個任務:

執行者框架提供ThreadPoolExecutor類,使用池中的線程來執行Callable和Runnable任務,這樣可以避免所有線程的創建操作。當你提交一個任務給執行者,會根據執行者的配置儘快執行它。在有些使用情況下,當你對儘快執行任務不感覺興趣。你可能想要在一段時間之後執行任務或週期性地執行任務。基於這些目的,執行者框架提供 ScheduledThreadPoolExecutor類。
schedule(Callable<V> callable, long delay, TimeUnit unit)

7. 執行者週期性地運行一個任務:

scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

8. 執行者取消一個任務:

當你使用執行者工作時,你不得不管理線程。你只實現Runnable或 Callable任務和把它們提交給執行者。執行者負責創建線程,在線程池中管理它們,當它們不需要時,結束它們。有時候,你想要取消已經提交給執行者 的任務。在這種情況下,你可以使用Future的cancel()方法,它允許你做取消操作。


9. 執行者控制一個任務完成:

FutureTask類提供一個done()方法,允許你在執行者執行任務完成後執行一些代碼。你可以用來做一些後處理操作,生成一個報告,通過e-mail發送結果,或釋放一些資源。當執行的任務由FutureTask來控制完成,FutureTask會內部調用這個方法。這個方法在任務的結果設置和它的狀態變成isDone狀態之後被調用,不管任務是否已經被取消或正常完成。

10. 執行者分離任務的啓動和結果的處理:

通常,當你使用執行者執行併發任務時,你將會提交 Runnable或Callable任務給這個執行者,並獲取Future對象控制這個方法。你可以發現這種情況,你需要提交任務給執行者在一個對象中,而處理結果在另一個對象中。基於這種情況,Java併發API提供CompletionService類。

CompletionService 類有一個方法來提交任務給執行者和另一個方法來獲取已完成執行的下個任務的Future對象。在內部實現中,它使用Executor對象執行任務。這種行爲的優點是共享一個CompletionService對象,並提交任務給執行者,這樣其他(對象)可以處理結果。其侷限性是,第二個對象只能獲取那些已經完成它們的執行的任務的Future對象,所以,這些Future對象只能獲取任務的結果。

11. 執行者控制被拒絕的任務:

當你想要結束執行者的執行,你使用shutdown()方法來表明它的結束。執行者等待正在運行或等待它的執行的任務的結束,然後結束它們的執行。
如果你在shutdown()方法和執行者結束之間,提交任務給執行者,這個任務將被拒絕,因爲執行者不再接收新的任務。ThreadPoolExecutor類提供一種機制,在調用shutdown()後,不接受新的任務。
通過實現RejectedExecutionHandler,在執行者中管理拒絕任務。



參考資料:《Java 7 Concurrency Cookbook》

                 《Java 9 Concurrency Cookbook Second Edition》


發佈了70 篇原創文章 · 獲贊 28 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章