Java 執行緒池詳解

低吟不作語發表於2021-03-31

本文部分摘自《Java 併發程式設計的藝術》


概述

Java 中的執行緒池是執行場景最多的併發框架,合理使用執行緒池能夠帶來三個好處:

  • 降低資源消耗。通過重複利用已有的執行緒降低執行緒建立和銷燬造成的消耗
  • 提高響應速度。當任務到達時,任務可以不需要等待執行緒建立就能立即執行
  • 提高執行緒可管理性。執行緒是稀缺資源,使用執行緒池進行統一分配、調優和監控,可以降低資源消耗,提高系統穩定性

執行緒池的實現原理

從圖中可以看到,當提交一個新任務到執行緒池時,執行緒池的處理流程如下:

  1. 執行緒池判斷核心執行緒池裡的執行緒是否都在執行任務,如果不是,建立一個新的工作執行緒執行任務,否則進入下一流程
  2. 執行緒池判斷工作佇列是否已滿,如果工作佇列沒有滿,將新提交的任務儲存在工作佇列中,否則進入下一流程
  3. 執行緒池判斷執行緒池裡的執行緒是否都處於工作狀態,如果沒有,建立一個新的工作執行緒執行任務,否則交給飽和策略來處理這個任務

使用執行緒池

1. 建立執行緒池

我們可以通過 ThreadPoolExecutor 來建立一個執行緒池

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);

建立一個執行緒需要輸入幾個引數,如下:

  • corePoolSize(執行緒池的基本大小)

    當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即時其他空閒的基本執行緒能夠執行新任務也會建立執行緒,等到需要執行的任務數大於執行緒池基本大小時就不再建立

  • maximumPoolSize(執行緒池最大數量)

    執行緒池允許建立的最大執行緒數,如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是,如果使用無界阻塞佇列做任務佇列,則這個引數沒有什麼效果

  • keepAliveTime(執行緒活動保持時間)

    執行緒池的工作執行緒空閒後,保持存活的時間。如果任務很多,並且每個任務的執行時間都比較短,可以調大時間,提高執行緒利用率

  • unit(執行緒保持活動時間的單位)

    可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微妙(MICROSECONDS)和納秒(NANOSECONDS)

  • workQueue(任務佇列)

    用於儲存等到執行的任務的阻塞佇列,可以選擇以下幾個阻塞佇列:

    • ArrayBlockingQueue

      是一個基於陣列結構的有界阻塞佇列,此佇列按 FIFO(先進先出)原則對元素進行排序

    • LinkedBlockingQueue

      一個基於連結串列結構的阻塞佇列,此佇列按 FIFO 排序元素,吞吐量通常高於 ArrayBlockingQueue

    • SynchronousQueue

      一個不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一致處於阻塞狀態,吞吐量通常要高於 LinkedBlockingQueue

    • PriorityBlockingQueue

      一個具有優先順序的無界阻塞佇列

  • threadFactory

    用於設定建立執行緒的工廠,可以通過執行緒工廠給每個建立出來的執行緒設定更有意義的名字

  • handler(飽和策略)

    當任務和執行緒池都滿了,說明執行緒池處於飽和狀態,必須採取一種策略處理提交的新任務。在 JDK5 中執行緒池框架提供了以下四種策略:

    • AbortPolicy:直接丟擲異常,預設採取這種策略
    • CallerRunsPolicy:使用呼叫者所線上程來執行任務
    • DiscardOldestPolicy:丟棄佇列最近的一個任務,並執行當前任務
    • DiscardPolicy:不處理,丟棄掉

    也可以根據需要實現 RejectedExecutionHandler 介面自定義策略

2. 向執行緒池提交任務

可以使用 execute() 和 submit() 方法向執行緒池提交任務

  • execute() 方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功

    threadsPool.execute(new Runnable() {
        @Override
        public void run() {
            //...
        }
    })
    
  • submit() 方法用於提交需要返回值的任務,執行緒池會返回一個 future 物件,通過這個物件可以判斷任務是否執行成功

    Future<Object> future = executor.submit(hasReturnValueTask);
    try {
        Object s = future.get();
    } catch(InterruptedException e) {
        // 處理中斷異常
    } catch(ExecutionException e) {
        // 處理無法執行任務異常
    } finally {
        // 關閉執行緒池
        executor.shutdown();
    }
    

3. 關閉執行緒池

可以通過呼叫執行緒池的 shutdown 或 shutdownNow 方法來關閉執行緒池,它們的原理是遍歷執行緒池中的工作執行緒,逐個呼叫執行緒的 interrupt 方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止

shutdown 方法和 shutdownNow 方法存在一定的區別:

  • shutdownNow 方法首先將執行緒池狀態設定成 STOP,然後嘗試停止所有正在執行或暫停任務的執行緒,並返回等待執行任務的列表
  • shutdown 方法只是將執行緒池狀態設定成 SHUTDOWN 狀態,然後中斷所有沒有正在執行任務的執行緒

只要呼叫了這兩個關閉方法中的任意一個,isShutdown 方法就會返回 true,當所有任務都已關閉,才表示執行緒池關閉成功,這時呼叫 isTerminaed 方法會返回 true。至於應該採用哪種方法關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫 shutdown 方法關閉執行緒池,如果任務不一定要執行完成,可以呼叫 shutdownNow 方法


基於執行緒池技術的簡單 Web 伺服器

目前的瀏覽器都支援多執行緒訪問,比如請求一個頁面的時候,頁面包含的圖片等靜態資源會被瀏覽器併發的獲取。如果 Web 伺服器是單執行緒的,按順序處理髮送過來的請求,無疑會影響使用者體驗,因此大部分 Web 伺服器都支援併發訪問

下面使用執行緒池來構造一個簡單的 Web 伺服器,這個 Web 伺服器用來處理 HTTP 請求,目前只能處理簡單的文字和圖片內容。該 Web 伺服器使用 main 執行緒不斷接受客戶端的 Socket 連線,將連線以及請求提交給執行緒池處理,這樣使得 Web 伺服器能同時處理多個客戶端的請求

public class SimpleHttpServer {

    static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            5, 10, 60L,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    static ServerSocket serverSocket;

    static int port = 8080;

    public static void setPort(int port) {
        if (port > 0) {
            SimpleHttpServer.port = port;
        }
    }

    /**
     * 啟動 SimpleHttpServer
     */
    public static void start() throws Exception {
        serverSocket = new ServerSocket(port);
        Socket socket = null;
        while ((socket = serverSocket.accept()) != null) {
            // 接收一個客戶端Socket,生成一個HttpRequestHandler,放入執行緒池執行
            threadPool.execute(new HttpRequestHandler(socket));
        }
        serverSocket.close();
    }

    static class HttpRequestHandler implements Runnable {

        private Socket socket;

        public HttpRequestHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            String line;
            BufferedReader br = null;
            BufferedReader reader = null;
            PrintWriter out = null;
            InputStream in = null;
            try {
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String header = reader.readLine();
                // 計算絕對路徑
                String filePath = SimpleHttpServer.class.getResource(header.split(" ")[1]).getPath();
                out = new PrintWriter(socket.getOutputStream());
                // 如果請求資源的字尾為 jpg 或 ico,則讀取資源並輸出
                if (filePath.endsWith("jpg") || filePath.endsWith("ico")) {
                    in = new FileInputStream(filePath);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int i;
                    while ((i = in.read()) != -1) {
                        baos.write(i);
                    }
                    byte[] array = baos.toByteArray();
                    out.println("HTTP/1.1 200 OK");
                    out.println("Server: YeeQ");
                    out.println("Content-Type: image/jpeg");
                    out.println("Content-Length: " + array.length);
                    out.println("");
                    socket.getOutputStream().write(array, 0, array.length);
                } else {
                    br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
                    out = new PrintWriter(socket.getOutputStream());
                    out.println("HTTP/1.1 200 OK");
                    out.println("Server: YeeQ");
                    out.println("Content-Type: text/html; charset=UTF-8");
                    out.println("");
                    while ((line = br.readLine()) != null) {
                        out.println(line);
                    }
                }
                out.flush();
            } catch (Exception e) {
                if (out != null) {
                    out.println("HTTP/1.1 500");
                    out.println("");
                    out.flush();
                }
            } finally {
                close(br, in, reader, out, socket);
            }
        }
    }

    /**
     * 關閉流或者socket
     */
    private static void close(Closeable... closeables) {
        if (closeables != null) {
            for (Closeable closeable : closeables) {
                if (closeable != null) {
                    try {
                        closeable.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        SimpleHttpServer.start();
    }
}

相關文章