併發編程系列(七):線程池原理

一、線程池
在java中,創建和銷燬線程花費的時間和消耗的資源都較大,如果每來一個請求就創建一個線程,可能會導致系統資源的過渡消耗。爲了解決該問題,引入了線程池。
通過創建一個線程池子來管理多個線程的使用,當有任務需要處理,則分配給線程池中的線程處理,線程處理完後不會立即銷燬,而是等待後續任務。通過對線程的管理,避免大量線程創建的開銷

線程池的優勢:
1. 降低創建線程和銷燬線程的性能開銷
2. 提高響應速度,當有新任務需要執行是不需要等待線程創建就可以立馬執行
3. 合理的設置線程池大小可以避免因爲線程數超過硬件資源瓶頸帶來的問題

 

二、java提供的線程池API

1.線程池基本使用

public class Test implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
    static ExecutorService service=Executors.newFixedThreadPool(3);
    public static void main(String[] args) {
        for(int i=0;i<100;i++) {
            service.execute(new Test());
        }
        service.shutdown();
    }
}

2.Java  中提供的線程池  Api
爲了方便大家對於線程池的使用,在 Executors 裏面提供了幾個線程池的工廠方法,這樣很多新手就不需要了解太多關於 ThreadPoolExecutor 的知識了,他們只需要直接使用 Executors 的工廠方法,就可以使用線程池:

  • newFixedThreadPool:該方法返回一個固定數量的線程池,線程數不變,當有一個任務提交時,若線程池中空閒,則立即執行,若沒有,則會被暫緩在一個任務隊列中,等待有空閒的線程去執行。
  • newSingleThreadExecutor: 創建一個線程的線程池,若空閒則執行,若沒有空閒線程則暫緩在任務隊列中。
  • newCachedThreadPool:返回一個可根據實際情況調整線程個數的線程池,不限制最大線程數量,若用空閒的線程則執行任務,若無任務則不創建線程。並且每一個空閒線程會在 60 秒後自動回收
  • newScheduledThreadPool: 創建一個可以指定線程的數量的線程池,但是這個線程池還帶有延遲和週期性執行任務的功能,類似定時器。

3.ThreadpoolExecutor
問題:請簡單說下你知道的線程池和 ThreadpoolExecutor 有哪些構造參數?
上面提到的四種線程池的構建,都是基於 ThreadpoolExecutor 來構建的。

eg.ThreadpoolExecutor最完整構造方法各參數意義

public ThreadPoolExecutor(int corePoolSize, // 核心線程數量
                        int maximumPoolSize, // 最大線程數
                        long keepAliveTime, // 超時時間,超出核心線程數量以外的線程空餘存活時間
                        TimeUnit unit, // 存活時間單位
                        BlockingQueue<Runnable> workQueue, // 保存執行任務的隊列
                        ThreadFactory threadFactory, // 創建新線程使用的工廠
                        RejectedExecutionHandler handler // 當任務無法執行的時候的處理方式)

1) 核心線程數與最大線程數區別

核心線程表示主要使用的線程,超過核心線程而又小於最大線程數部分的線程相當於臨時線程,不會長期維護和使用。兩者相當於正式工與臨時工的區別。

2) RejectedExecutionHandler

拒絕策略,當超過最大線程數後的線程處理方式。

eg.newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

這個線程池執行流程如下:

1) 線程數少於核心線程數,也就是設置的線程數時,新建線程執行任務
2) 線程數等於核心線程數後,將任務加入阻塞隊列
3) 由於隊列容量非常大,可以一直添加
4) 執行完任務的線程反覆去隊列中取任務執行
用途:FixedThreadPool 用於負載比較大的服務器,爲了資源的合理利用,需要限制當前線程數量.

eg.newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

它的執行流程如下:
1) 沒有核心線程,直接向 SynchronousQueue 中提交任務
2) 如果有空閒線程,就去取出任務執行;如果沒有空閒線程,就新建一個
3) 執行完任務的線程有 60 秒生存時間,如果在這個時間內可以接到新任務,就可以繼續活下去,否則就被回收

eg.newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

 

三、線程池原理
eg.原理實現流程圖

判斷當前工作線程數是否大於核心線程,若小於則創建一個核心工作線程並將工作線程數+1;否則將新進的任務加入到阻塞隊列中。
創建的核心工作線程會不斷的從阻塞隊列中取出任務執行,若阻塞隊列沒有任務,則線程掛起阻塞狀態。
阻塞隊列如果沒滿,則將新任務加入隊列;否則,新增加工作線程去執行任務(臨時工作線程)。
若工作線程超過最大線程數,則不再增加新線程,執行拒絕策略。

 

ctl 的作用
在線程池中,ctl 貫穿在線程池的整個生命週期中
ctl: private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
它是一個原子類,主要作用是用來保存線程數量和線程池的狀態。它用到了位運算,一個 int 數值是 32 個 bit 位,這裏採用高 3 位來保存運行狀態,低 29 位來保存線程數量。

private static final int COUNT_BITS = Integer.SIZE - 3; // 32-3
private static final int CAPACITY = (1 << COUNT_BITS) - 1; //將 1 的二進制向右位移 29 位,再減 1 表示最大線程容量

// 運行狀態保存在 int 值的高 3 位 ( 所有數值左移 29 位 )
private static final int RUNNING = -1 << COUNT_BITS; // 接收新任務,並執行隊列中的任務
private static final int SHUTDOWN = 0 << COUNT_BITS; // 不接收新任務,但是執行隊列中的任務
private static final int STOP = 1 << COUNT_BITS; // 不接收新任務,不執行隊列中的任務,中斷正在執行中的任務
private static final int TIDYING = 2 << COUNT_BITS; // 所有的任務都已結束,線程數量爲 0,處於該狀態的線程池即將調用 terminated()方法
private static final int TERMINATED = 3 << COUNT_BITS; // terminated()方法執行完成

eg.狀態轉化圖

Worker類
封裝了 firstTask 和 thread 參數。

  • firstTask,線程運行第一次執行的任務
  • thread,worker類真正運行的線程,將worker(實現了Runnable接口)當做參數啓動
  • runWorker方法,工作線程執行任務的真正邏輯

 

線程釋放

在執行 execute 方法時,如果當前線程池的線程數量超過了 corePoolSize 且小於 maximumPoolSize,並且 workQueue 已滿時,則可以增加工作線程,但這時如果超時沒有獲取到任務,也就是 timedOut 爲 true 的情況,說明 workQueue 已經爲空了,也就說明了當前線程池中不需要那麼多線程來執行任務了,可以把多於 corePoolSize 數量的線程銷燬掉,保持線程數量在 corePoolSize 即可。
什麼時候會銷燬?在 runWorker 方法執行完之後,也就是 Worker 中的 run 方法執行完,由 JVM 自動回收。
getTask 方法返回 null 時,在 runWorker 方法中會跳出 while 循環,然後會執行 processWorkerExit 方法。

 

拒絕策略
如果非核心線程數也達到了最大線程數大小,則根據不同的策略拒絕任務。拒絕策略如下:

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

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


四、線程池注意事項


五、Future/Callable
線程池的執行任務有兩種方法,一種是 submit、一種是 execute。這兩個方法是有區別的:

序號 execute submit
1 execute 只可以接收一個 Runnable 的參數 submit 可以接收 Runable 和 Callable 這兩種類型的參數
2 execute 如果出現異常會拋出 對於 submit 方法,如果傳入一個 Callable,可以得到一個 Future 的返回值
3 execute 沒有返回值 submit 方法調用不會拋異常,除非調用 Future.get

Callablee /Future  案例演示
Callable/Future 和 Thread 之類的線程構建最大的區別在於,能夠很方便的獲取線程執行完以後的結果。

public class CallableDemo implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(3000); // 阻塞案例演示
        return "hello world";
    }
    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        CallableDemo callableDemo = new CallableDemo();
        FutureTask futureTask = new FutureTask(callableDemo);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

爲什麼需要使用回調?

因爲結果值是由另一線程計算的,當前線程是不知道結果值什麼時候計算完成,所以它傳遞一個回調接口給計算線程,當計算完成時,調用這個回調接口,回傳結果值。
這個在很多地方有用到,比如 Dubbo 的異步調用,比如消息中間件的異步通信等等。利用 FutureTask、Callable、Thread 對耗時任務(如查詢數據庫)做預處理,在需要計算結果之前就啓動計算。

 

Callable /Future源碼分析

在剛剛實現的 demo 中,我們用到了兩個 api,分別是 Callable 和 FutureTask。

Callable

Callable 是一個函數式接口,裏面就只有一個 call 方法。子類可以重寫這個方法,並且這個方法會有一個返回值

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

FutureTask
FutureTask 的類關係圖如下:

它實現 RunnableFuture 接口,那麼這個 RunnableFuture 接口的作用是什麼呢。

@FunctionalInterface
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

RunnableFuture 是一個接口,它繼承了 Runnable 和 Future 這兩個接口,Runnable 比較熟悉了,那麼 Future 是什麼呢?
Future 表示一個任務的生命週期,並提供了相應的方法來判斷是否已經完成或取消,以及獲取任務的結果和取消任務等。

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    // 當前的 Future 是否被取消,返回 true 表示已取消
    boolean isCancelled();
    // 當前 Future 是否已結束。包括運行完成、拋出異常以及取消,都表示當前 Future 已結束
    boolean isDone();
    // 獲取 Future 的結果值。如果當前 Future 還沒有結束,那麼當前線程就等待,
    // 直到 Future 運行結束,那麼會喚醒等待結果值的線程的。
    V get() throws InterruptedException, ExecutionException;
    // 獲取 Future 的結果值。與 get() 相比較多了允許設置超時時間
    V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

分析到這裏我們其實有一些初步的頭緒了,FutureTask 是 Runnable 和 Future 的結合,如果我們把 Runnable 比作是生產者,Future 比作是消費者,那麼 FutureTask 是被這兩者共享的,生產者運行 run 方法計算結果,消費者通過 get 方法獲取結果。
作爲生產者消費者模式,有一個很重要的機制,就是如果生產者數據還沒準備的時候,消費者會被阻塞。當生產者數據準備好了以後會喚醒消費者繼續執行。


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章