JAVA併發編程 併發容器 線程池 Executor 使用以及深入瞭解

線程池

爲什麼要用線程池?
Java 中的線程池是運用場景最多的併發框架,幾乎所有需要異步或併發執行 任務的程序都可以使用線程池。在開發過程中,合理地使用線程池能夠帶來 3 個好處。

  • 第一:降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成 的消耗。
  • 第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立 即執行。

假設一個服務器完成一項任務所需時間爲:T1 創建線程時間,T2 在線 程中執行任務的時間,T3 銷燬線程時間。如果:T1+T3 遠大於T2,則可以採用線程池,以提高服務器性能。線程池技術正是關注如何縮短或調整 T1,T3 時 間的技術,從而提高服務器程序性能的。它把T1,T3 分別安排在服務器程序的 啓動和結束的時間段或者一些空閒的時間段,這樣在服務器程序處理客戶請求時, 不會有 T1,T3 的開銷了。

  • 第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會 消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和 監控。

假設一個服務器一天要處理 50000 個請求,並且每個請求需要一個單獨的線程完成。在線程池中,線程數一般是固定的,所以產生線程總數不會超過線程池 中線程的數目,而如果服務器不利用線程池來處理這些請求則線程總數爲50000。 一般線程池大小是遠小於 50000。所以利用線程池的服務器程序不會爲了創建 50000 而在處理請求時浪費時間,從而提高效率。

手寫線程池

/**
 * 類說明:自定義線程池實現
 */
public class MyThreadPool2 {

    /*缺省線程數據量*/
    private static int WORK_COUNT = 5;

    /*存放任務*/
    private final BlockingQueue<Runnable> taskQueue;
    /*工作線程*/
    private WorkThread[] workThreads;
    private final int work_number;

    public MyThreadPool2() {
        this(100, WORK_COUNT);
    }

    /*任務數,線程的數量*/
    public MyThreadPool2(int task_count,
                         int work_number) {
        if (work_number <= 0) {
            work_number = WORK_COUNT;
        }
        if (task_count <= 0) {
            task_count = 100;
        }
        this.taskQueue = new ArrayBlockingQueue<>(task_count);
        this.work_number = work_number;
        workThreads = new WorkThread[work_number];
        /*工作線程準備好了*/
        for (int i = 0; i < work_number; i++) {
            workThreads[i] = new WorkThread();
            workThreads[i].start();
        }
    }

    /*銷燬線程池*/
    public void destroy() {
        System.out.println("ready close pool....");
        for (int i = 0; i < work_number; i++) {
            workThreads[i].stopWorker();
            workThreads[i] = null;//help gc
        }
        taskQueue.clear();
    }

    /*放入任務,但是隻是加入隊列*/
    public void execute(Runnable task) {
        try {
            taskQueue.put(task);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    @Override
    public String toString() {
        return "WorkThread number:" + work_number
                + " wait task number:" + taskQueue.size();
    }

    /*內部類,工作線程的實現*/
    private class WorkThread extends Thread {
        @Override
        public void run() {
            Runnable r = null;
            try {
                while (!isInterrupted()) {
                    r = taskQueue.take();
                    if (r != null) {
                        System.out.println(getId() + " ready execute"
                                + ((TestMyThreadPool.MyTask) r).getName());
                        r.run();
                    }
                    r = null;
                }
            } catch (InterruptedException e) {
                //e.printStackTrace();
            }

        }

        /*停止工作*/
        public void stopWorker() {
            interrupt();
        }
    }


}

測試自定義線程池實現

/**
 *類說明:測試自定義線程池實現
 */
public class TestMyThreadPool {
    public static void main(String[] args) throws InterruptedException {
//         創建3個線程的線程池
        MyThreadPool2 t = new MyThreadPool2(0,3);
        t.execute(new MyTask("testA"));
        t.execute(new MyTask("testB"));
        t.execute(new MyTask("testC"));
        t.execute(new MyTask("testD"));
        t.execute(new MyTask("testE"));
        System.out.println(t);
        Thread.sleep(10000);
        t.destroy();// 所有線程都執行完成才destory
        System.out.println(t);
    }

    // 任務類
    static class MyTask implements Runnable {

        private String name;
        private Random r = new Random();

        public MyTask(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public void run() {// 執行任務
            //int x =10;
            try {
                Thread.sleep(r.nextInt(1000)+2000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getId()+" sleep InterruptedException:"
                        +Thread.currentThread().isInterrupted());
            }
            System.out.println("任務 " + name + " 完成");
        }
    }
}

Executor框架

在這裏插入圖片描述

ThreadPoolExecutor 的類關係

在這裏插入圖片描述
Executor 是一個接口,它是 Executor 框架的基礎,它將任務的提交與任務的 執行分離開來。

ExecutorService 接口繼承了 Executor,在其上做了一些 shutdown()、submit() 的擴展,可以說是真正的線程池接口;

  • AbstractExecutorService 抽象類實現了 ExecutorService 接口中的大部分方法;
  • ThreadPoolExecutor 是線程池的核心實現類,用來執行被提交的任務。
  • ScheduledExecutorService 接口繼承了 ExecutorService 接口,提供了帶"週期 執行"功能 ExecutorService;
  • ScheduledThreadPoolExecutor是一個實現類,可以在給定的延遲後運行命令, 或者定期執行命令。
  • ScheduledThreadPoolExecutor 比 Timer 更靈活,功能更強大。

線程池的創建各個參數含義

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable>workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

corePoolSize

  • 線程池中的核心線程數,當提交一個任務時,線程池創建一個新線程執行任 務,直到當前線程數等於 corePoolSize;
  • 如果當前線程數爲 corePoolSize,繼續提交的任務被保存到阻塞隊列中,等 待被執行;
  • 如果執行了線程池的 prestartAllCoreThreads()方法,線程池會提前創建並啓 動所有核心線程。

maximumPoolSize
線程池中允許的最大線程數。如果當前阻塞隊列滿了,且繼續提交任務,則 創建新的線程執行任務,前提是當前線程數小於 maximumPoolSize

keepAliveTime
線程空閒時的存活時間,即當線程沒有任務執行時,繼續存活的時間。默認 情況下,該參數只在線程數大於 corePoolSize 時纔有用

  • TimeUnit
  • keepAliveTime 的時間單位
  • workQueue
    workQueue 必須是 BlockingQueue 阻塞隊列。當線程池中的線程數超過它的 corePoolSize 的時候,線程會進入阻塞隊列進行阻塞等待。通過 workQueue,線程池實現了阻塞功能

workQueue
用於保存等待執行的任務的阻塞隊列,一般來說,我們應該儘量使用有界隊 列,因爲使用無界隊列作爲工作隊列會對線程池帶來如下影響。

  • 當線程池中的線程數達到 corePoolSize 後,新任務將在無界隊列中等待, 因此線程池中的線程數不會超過 corePoolSize。
  • 由於 1,使用無界隊列時 maximumPoolSize 將是一個無效參數。
  • 由於 1 和 2,使用無界隊列時 keepAliveTime 將是一個無效參數。
  • 更重要的,使用無界 queue 可能會耗盡系統資源,有界隊列則有助於防 止資源耗盡,同時即使使用有界隊列,也要儘量控制隊列的大小在一個合適的範圍。

所以我們一般會使用,ArrayBlockingQueue、LinkedBlockingQueue、 SynchronousQueue、PriorityBlockingQueue。

threadFactory
創建線程的工廠,通過自定義的線程工廠可以給每個新建的線程設置一個具 有識別度的線程名,當然還可以更加自由的對線程做更多的設置,比如設置所有 的線程爲守護線程。

/**
 * 類說明:自定義線程池中線程的創建方式,把線程設置爲守護線程
 */
public class ThreadPoolAdv {
    static class Worker implements Runnable {
        private String taskName;
        private Random r = new Random();

        public Worker(String taskName) {
            this.taskName = taskName;
        }

        public String getName() {
            return taskName;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()
                    + " process the task : " + taskName);
            SleepTools.ms(r.nextInt(100) * 5);
        }
    }

    private static class MyThreadFactory implements ThreadFactory {

        private AtomicInteger count = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "Mark_" + count.getAndIncrement());
            t.setDaemon(true);
            System.out.println("create " + t);
            return t;
        }
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService threadPool = new ThreadPoolExecutor(2,
                4, 3,
                TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10),
                new MyThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());

        for (int i = 0; i <= 6; i++) {
            Worker worker = new Worker("worker " + i);
            System.out.println("A new task has been added : " + worker.getName());
            threadPool.execute(worker);
        }

    }
}

Executors 靜態工廠裏默認的 threadFactory,線程的命名規則是“pool-數字 -thread-數字”。

RejectedExecutionHandler

線程池的飽和策略,當阻塞隊列滿了,且沒有空閒的工作線程,如果繼續提 交任務,必須採取一種策略處理該任務,線程池提供了 4 種策略:

  • AbortPolicy:直接拋出異常,默認策略;
  • CallerRunsPolicy:用調用者所在的線程來執行任務;
  • DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,並執行當前任務;
  • DiscardPolicy:直接丟棄任務;

當然也可以根據應用場景實現 RejectedExecutionHandler 接口,自定義飽和 策略,如記錄日誌或持久化存儲不能處理的任務。

擴展線程池

能擴展線程池的功能嗎?比如在任務執行的前後做一點我們自己的業務工 作?實際上,JDK 的線程池已經爲我們預留的接口,在線程池核心方法中,有 2 個方法是空的,就是給我們預留的。還有一個線程池退出時會調用的方法。
參見代碼

/**
 * 類說明:擴展線程池的使用範例
 */
public class ThreadPoolExt {
    static class Worker implements Runnable {
        private String taskName;
        private Random r = new Random();

        public Worker(String taskName) {
            this.taskName = taskName;
        }

        public String getName() {
            return taskName;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()
                    + " process the task : " + taskName);
            SleepTools.ms(r.nextInt(100) * 5);
        }
    }


    public static void main(String[] args)
            throws InterruptedException, ExecutionException {
        ExecutorService threadPool = new ThreadPoolExecutor(2, 4, 3,
                TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10),
                new ThreadPoolExecutor.DiscardOldestPolicy()) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("Ready Execute " + ((Worker) r).getName());
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println("Complete Execute " + ((Worker) r).getName());
            }

            @Override
            protected void terminated() {
                System.out.println("線程池退出 ");
            }
        };

        for (int i = 0; i <= 6; i++) {
            Worker worker = new Worker("worker " + i);
            System.out.println("A new task has been added : " + worker.getName());
            threadPool.execute(worker);
        }
        threadPool.shutdown();

    }
}

可以看到,每個任務執行前後都會調用 beforeExecute 和 afterExecute 方法。 相當於執行了一個切面。而在調用 shutdown 方法後則會調用 terminated 方法。

線程池的工作機制

  1. 如果當前運行的線程少於 corePoolSize,則創建新線程來執行任務(注意,執行這一步驟需要獲取全局鎖)。
  2. 如果運行的線程等於或多於 corePoolSize,則將任務加入 BlockingQueue。
  3. 如果無法將任務加入 BlockingQueue(隊列已滿),則創建新的線程來處理任務。
  4. 如果創建新線程將使當前運行的線程超出 maximumPoolSize,任務將被拒絕,並調用 RejectedExecutionHandler.rejectedExecution()方法。

提交任務

  • execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程 池執行成功。
  • submit()方法用於提交需要返回值的任務。線程池會返回一個 future 類型的 對象,通過這個 future 對象可以判斷任務是否執行成功,並且可以通過 future 的 get()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用 get (longtimeout,TimeUnitunit)方法則會阻塞當前線程一段時間後立即返回,這 時候有可能任務沒有執行完。

關閉線程池

  • 可以通過調用線程池的 shutdown 或 shutdownNow 方法來關閉線程池。它們的原理是遍歷線程池中的工作線程,然後逐個調用線程的 interrupt 方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別, shutdownNow 首先將線程池的狀態設置成 STOP,然後嘗試停止所有的正在執行 或暫停任務的線程,並返回等待執行任務的列表,而 shutdown 只是將線程池的 狀態設置成 SHUTDOWN 狀態,然後中斷所有沒有正在執行任務的線程
  • 只要調用了這兩個關閉方法中的任意一個,isShutdown 方法就會返回 true。 當所有的任務都已關閉後,才表示線程池關閉成功,這時調用 isTerminaed 方法 會返回 true。至於應該調用哪一種方法來關閉線程池,應該由提交到線程池的任 務特性決定,通常調用 shutdown 方法來關閉線程池,如果任務不一定要執行完, 則可以調用 shutdownNow 方法。

合理地配置線程池

要想合理地配置線程池,就必須首先分析任務特性,可以從以下幾個角度來分析:

  • 任務的性質:CPU 密集型任務、IO 密集型任務和混合型任務。
  • 任務的優先級:高、中和低。
  • 任務的執行時間:長、中和短。
  • 任務的依賴性:是否依賴其他系統資源,如數據庫連接。

性質不同的任務可以用不同規模的線程池分開處理。
CPU 密集型任務應配置儘可能小的線程,如配置 Ncpu+1 個線程的線程池。 由於 IO 密集型任務線程並不是一直在執行任務,則應配置儘可能多的線程,如 2*Ncpu。

混合型的任務,如果可以拆分,將其拆分成一個 CPU 密集型任務和一個 IO 密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐 量將高於串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行 分解。可以通過 Runtime.getRuntime().availableProcessors()方法獲得當前設備的 CPU 個數。

對於 IO 型的任務的最佳線程數,有個公式可以計算 Nthreads=NCPUUCPU(1+W/C)**
其中:
❑NCPU CPU核心數
❑UCPU 是期望的 CPU 利用率(該值應該介於 0 和 1 之間)
❑W/C 是等待時間與計算時間的比率

  • 等待時間與計算時間我們在Linux下使用相關的vmstat命令或者top命令查看。
  • 優先級不同的任務可以使用優先級隊列 PriorityBlockingQueue 來處理。它可以讓優先級高的任務先執行。
  • 執行時間不同的任務可以交給不同規模的線程池來處理,或者可以使用優先 級隊列,讓執行時間短的任務先執行。

依賴數據庫連接池的任務,因爲線程提交 SQL 後需要等待數據庫返回結果, 等待的時間越長,則 CPU 空閒時間就越長,那麼線程數應該設置得越大,這樣 才能更好地利用 CPU。建議使用有界隊列。有界隊列能增加系統的穩定性和預警能力,可以根據需 要設大一點兒,比如幾千。

假設,我們現在有一個 Web 系統,裏面使用了線程池來處理業務,在某些 情況下,系統裏後臺任務線程池的隊列和線程池全滿了,不斷拋出拋棄任務的異 常,通過排查發現是數據庫出現了問題,導致執行 SQL 變得非常緩慢,因爲後臺 任務線程池裏的任務全是需要向數據庫查詢和插入數據的,所以導致線程池裏的 工作線程全部阻塞,任務積壓在線程池裏。 如果當時我們設置成無界隊列,那麼線程池的隊列就會越來越多,有可能會撐滿內存,導致整個系統不可用,而不只是後臺任務出現問題。

預定義線程池

/**
 * 類說明:線程池的使用範例
 */
public class UseThreadPool {
    /*沒有返回值*/
    static class Worker implements Runnable {
        private String taskName;
        private Random r = new Random();

        public Worker(String taskName) {
            this.taskName = taskName;
        }

        public String getName() {
            return taskName;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()
                    + " process the task : " + taskName);
            SleepTools.ms(r.nextInt(100) * 5);
        }
    }

    /*有返回值*/
    static class CallWorker implements Callable<String> {

        private String taskName;
        private Random r = new Random();

        public CallWorker(String taskName) {
            this.taskName = taskName;
        }

        public String getName() {
            return taskName;
        }

        @Override
        public String call() throws Exception {
            System.out.println(Thread.currentThread().getName()
                    + " process the task : " + taskName);
            return Thread.currentThread().getName() + ":" + r.nextInt(100) * 5;
        }

    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Runtime.getRuntime().availableProcessors();//邏輯核心
//        ExecutorService threadPool = new ThreadPoolExecutor(2,
//                4,3,TimeUnit.SECONDS,
//                new ArrayBlockingQueue<>(10),
//                new ThreadPoolExecutor.DiscardOldestPolicy());
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        ExecutorService threadPool1 = Executors.newSingleThreadExecutor();
        ExecutorService threadPool2 = Executors.newCachedThreadPool();
        ExecutorService threadPool3 = Executors.newWorkStealingPool();
//        ExecutorService t  hreadPool4 = Executors.newScheduledThreadPool();
//        ExecutorService threadPool5 = Executors.newSingleThreadScheduledExecutor()


        for (int i = 0; i <= 6; i++) {
            Worker worker = new Worker("worker " + i);
            System.out.println("A new task has been added : " + worker.getName());
            threadPool.execute(worker);
        }

        for (int i = 0; i <= 6; i++) {
            CallWorker callWorker = new CallWorker("worker " + i);
            System.out.println("A new task has been added : " + callWorker.getName());
            Future<String> result = threadPool.submit(callWorker);
            System.out.println(result.get());
        }
        threadPool.shutdown();
        threadPool.shutdownNow();
    }
}

FixedThreadPool詳解

  • 創建使用固定線程數的 FixedThreadPool 的 API。適用於爲了滿足資源管理的 需求,而需要限制當前線程數量的應用場景,它適用於負載比較重的服務器。 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被設置爲創建 FixedThreadPool 時指定的參數 nThreads。
  • 當線程池中的線程數大於 corePoolSize 時, keepAliveTime 爲多餘的空閒線程等待新任務的
  • 最長時間,超過這個時間後多餘的線程將被終止。這裏把 keepAliveTime 設 置爲 0L,意味着多餘的空閒線程會被立即終止。
  • FixedThreadPool 使用有界隊列 LinkedBlockingQueue 作爲線程池的工作隊列 (隊列的容量爲 Integer.MAX_VALUE)。

SingleThreadExecutor

  • 創建使用單個線程的 SingleThread-Executor 的 API,於需要保證順序地執行 各個任務;並且在任意時間點,不會有多個線程是活動的應用場景。
  • corePoolSize 和 maximumPoolSize 被設置爲 1。其他參數與 FixedThreadPool 相同。SingleThreadExecutor 使用有界隊列 LinkedBlockingQueue 作爲線程池的工 作隊列(隊列的容量爲 Integer.MAX_VALUE)。

CachedThreadPool

  • 創建一個會根據需要創建新線程的 CachedThreadPool 的 API。大小無界的線 程池,適用於執行很多的短期異步任務的小程序,或者是負載較輕的服務器。
  • corePoolSize 被設置爲 0,即 corePool 爲空;maximumPoolSize 被設置爲 Integer.MAX_VALUE。這裏把 keepAliveTime 設置爲 60L,意味着 CachedThreadPool 中的空閒線程等待新任務的最長時間爲60秒,空閒線程超過60秒後將會被終止。
  • FixedThreadPool 和 SingleThreadExecutor 使用有界隊列 LinkedBlockingQueue 作爲線程池的工作隊列。CachedThreadPool 使用沒有容量的 SynchronousQueue 作爲線程池的工作隊列,但 CachedThreadPool 的 maximumPool 是無界的。這意
    味着,如果主線程提交任務的速度高於 maximumPool 中線程處理任務的速度時, CachedThreadPool 會不斷創建新線程。極端情況下,CachedThreadPool 會因爲創 建過多線程而耗盡 CPU 和內存資源。

WorkStealingPool

利用所有運行的處理器數目來創建一個工作竊取的線程池,使用 forkjoin 實現

ScheduledThreadPoolExecutor

使用工廠類 Executors 來創建。Executors 可以創建 2 種類型的 ScheduledThreadPoolExecutor,如下:

  • ScheduledThreadPoolExecutor。包含若干個線程的 ScheduledThreadPoolExecutor。
  • SingleThreadScheduledExecutor。只包含一個線程的 ScheduledThreadPoolExecutor。

ScheduledThreadPoolExecutor 適用於需要多個後臺線程執行週期任務,同時 爲了滿足資源管理的需求而需要限制後臺線程的數量的應用場景。
SingleThreadScheduledExecutor 適用於需要單個後臺線程執行週期任務,同 時需要保證順序地執行各個任務的應用場景。

提交定時任務

//向定時任務線程池提交一個延時 Runnable 任務(僅執行一次) 
publicScheduledFuture<?>schedule(Runnablecommand,longdelay,TimeUnitunit)
//向定時任務線程池提交一個延時的 Callable 任務(僅執行一次)
public<V>ScheduledFuture<V>schedule(Callable<V>callable,longdelay, TimeUnitunit); 
//向定時任務線程池提交一個固定時間間隔執行的任務 
publicScheduledFuture<?>scheduleAtFixedRate(Runnablecommand,long initialDelay, longperiod,TimeUnitunit) 
//向定時任務線程池提交一個固定時間間隔執行的任務 
publicScheduledFuture<?>scheduleWithFixedDelay(Runnablecommand,long initialDelay, longdelay,TimeUnitunit); 

固定時間間隔的任務不論每次任務花費多少時間,下次任務開始執行時間從 理論上講是確定的,當然執行任務的時間不能超過執行週期。
固定延時間隔的任務是指每次執行完任務以後都延時一個固定的時間。由於 操作系統調度以及每次任務執行的語句可能不同,所以每次任務執行所花費的時 間是不確定的,也就導致了每次任務的執行週期存在一定的波動。

定時任務超時問題
scheduleAtFixedRate 中,若任務處理時長超出設置的定時頻率時長,本次任 務執行完纔開始下次任務,下次任務已經處於超時狀態,會馬上開始執行。
若任務處理時長小於定時頻率時長,任務執行完後,定時器等待,下次任務 會在定時器等待頻率時長後執行。
如下例子:
設置定時任務每 60s 執行一次,那麼從理論上應該第一次任務在第 0s 開始, 第二次任務在第 60s 開始,第三次任務在 120s 開始,但實際運行時第一次任務 時長 80s,第二次任務時長 30s,第三次任務時長 50s,則實際運行結果爲:

  • 第一次任務第 0s 開始,第 80s 結束;
  • 第二次任務第 80s 開始,第 110s 結束(上次任務已超時,本次不會再等待 60s, 會馬上開始);
  • 第三次任務第 120s 開始,第 170s 結束.
  • 第四次任務第 180s 開始…
/**
 *類說明:定時任務的工作類
 */
public class ScheduleWorkerTime implements Runnable{
    public final static int Long_8 = 8;//工作8秒
    public final static int Short_2 = 2;//工作2秒
    public final static int Normal_5 = 5;//工作5秒

    public static SimpleDateFormat formater = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");
    public static AtomicInteger count = new AtomicInteger(0);
    
    @Override
    public void run() {
    	if(count.get()==0) {
            System.out.println("Long_8....begin:"+formater.format(new Date()));
            SleepTools.second(Long_8);
            System.out.println("Long_8....end:"+formater.format(new Date())); 
            count.incrementAndGet();
    	}else if(count.get()==1) {
    		System.out.println("Short_2 ...begin:"+formater.format(new Date()));
    		SleepTools.second(Short_2);
    		System.out.println("Short_2 ...end:"+formater.format(new Date()));
            count.incrementAndGet();    		
    	}else {
    		System.out.println("Normal_5...begin:"+formater.format(new Date()));
    		SleepTools.second(Normal_5);
    		System.out.println("Normal_5...end:"+formater.format(new Date()));
    		count.incrementAndGet(); 
    	}
//    	if(taskType==Long_8) {
//            System.out.println("Long_8....begin:"+formater.format(new Date()));
//            SleepTools.second(Long_8);
//            System.out.println("Long_8....end:"+formater.format(new Date()));
//    	}else if(taskType==Short_2) {
//    		System.out.println("Short_2 ...begin:"+formater.format(new Date()));
//    		SleepTools.second(Short_2);
//    		System.out.println("Short_2 ...end:"+formater.format(new Date()));
//    	}else {
//    		System.out.println("Normal_5...begin:"+formater.format(new Date()));
//    		SleepTools.second(Normal_5);
//    		System.out.println("Normal_5...end:"+formater.format(new Date()));
//    	}
    }
}

CompletionService

CompletionService 實際上可以看做是 Executor 和 BlockingQueue 的結合體。 CompletionService 在接收到要執行的任務時,通過類似 BlockingQueue 的 put 和 take 獲得任務執行的結果。
CompletionService 的一個實現是 ExecutorCompletionService, ExecutorCompletionService 把具體的計算任務交給 Executor 完成。
在實現上,ExecutorCompletionService 在構造函數中會創建一個 BlockingQueue(使用的基於鏈表的 LinkedBlockingQueue),該 BlockingQueue 的 作用是保存 Executor 執行的結果。
當提交一個任務到 ExecutorCompletionService 時,首先將任務包裝成 QueueingFuture,它是 FutureTask 的一個子類,然後改寫 FutureTask 的 done 方 法,之後把 Executor 執行的計算結果放入 BlockingQueue 中。
與 ExecutorService 最主要的區別在於 submit 的 task 不一定是按照加入時的 順序完成的。CompletionService 對 ExecutorService 進行了包裝,內部維護一個保 存 Future 對象的 BlockingQueue。只有當這個 Future 對象狀態是結束的時候,才 會加入到這個 Queue 中, take()方法其實就是 Producer-Consumer 中的 Consumer。 它會從 Queue 中取出 Future 對象,如果 Queue 是空的,就會阻塞在那裏,直到 有完成的 Future 對象加入到 Queue 中。所以,先完成的必定先被取出。這樣就 減少了不必要的等待時間。

/**
 *類說明:CompletionService 
 */
public class CompletionCase {
    private final int POOL_SIZE = Runtime.getRuntime().availableProcessors();
    private final int TOTAL_TASK = Runtime.getRuntime().availableProcessors()*10;

    // 方法一,自己寫集合來實現獲取線程池中任務的返回結果
    public void testByQueue() throws Exception {
    	long start = System.currentTimeMillis();
    	AtomicInteger count = new AtomicInteger(0);
        // 創建線程池
        ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
        //隊列,拿任務的執行結果
        BlockingQueue<Future<Integer>> queue = 
        		new LinkedBlockingQueue<>();

        // 向裏面扔任務
        for (int i = 0; i < TOTAL_TASK; i++) {
            Future<Integer> future = pool.submit(new WorkTask("ExecTask" + i));
            queue.add(future);
        }

        // 檢查線程池任務執行結果
        for (int i = 0; i < TOTAL_TASK; i++) {
        	int sleptTime = queue.take().get();
        	//System.out.println(" slept "+sleptTime+" ms ...");        	
        	count.addAndGet(sleptTime);
        }

        // 關閉線程池
        pool.shutdown();
        System.out.println("-------------tasks sleep time "+count.get()
        		+"ms,and spend time "
        		+(System.currentTimeMillis()-start)+" ms");
    }

    public void testByCompletion() throws Exception{
        long start = System.currentTimeMillis();
        AtomicInteger count = new AtomicInteger(0);
        // 創建線程池
        ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
        CompletionService<Integer> cSevice
                = new ExecutorCompletionService<>(pool);

        // 向裏面扔任務
        for (int i = 0; i < TOTAL_TASK; i++) {
            cSevice.submit(new WorkTask("ExecTask" + i));
        }

        // 檢查線程池任務執行結果
        for (int i = 0; i < TOTAL_TASK; i++) {
            int sleptTime = cSevice.take().get();
            //System.out.println(" slept "+sleptTime+" ms ...");
            count.addAndGet(sleptTime);
        }

        // 關閉線程池
        pool.shutdown();
        System.out.println("-------------tasks sleep time "+count.get()
                +"ms,and spend time "
                +(System.currentTimeMillis()-start)+" ms");

    }

    public static void main(String[] args) throws Exception {
        CompletionCase t = new CompletionCase();
        t.testByQueue();
        t.testByCompletion();
    }
}

我們可以得出結論:

  • 使用方法一,自己創建一個集合來保存 Future 存根並循環調用其返回結果 的時候,主線程並不能保證首先獲得的是最先完成任務的線程返回值。它只是按 加入線程池的順序返回。因爲 take 方法是阻塞方法,後面的任務完成了,前面的任務卻沒有完成,主程序就那樣等待在那兒,只到前面的完成了,它才知道原 來後面的也完成了。
  • 使用方法二,使用 CompletionService 來維護處理線程不的返回結果時,主 線程總是能夠拿到最先完成的任務的返回值,而不管它們加入線程池的順序。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章