天天看點

面試突擊28:線程池有幾種建立方式?推薦使用哪種?

作者:磊哥聊程式設計

在 Java 語言中,并發程式設計都是通過建立線程池來實作的,而線程池的建立方式也有很多種,每種線程池的建立方式都對應了不同的使用場景,總體來說線程池的建立可以分為以下兩類:

  • 通過 ThreadPoolExecutor 手動建立線程池。
  • 通過 Executors 執行器自動建立線程池。
面試突擊28:線程池有幾種建立方式?推薦使用哪種?

而以上兩類建立線程池的方式,又有 7 種具體實作方法,這 7 種實作方法分别是:

  1. Executors.newFixedThreadPool:建立一個固定大小的線程池,可控制并發的線程數,超出的線程會在隊列中等待。
  2. Executors.newCachedThreadPool:建立一個可緩存的線程池,若線程數超過處理所需,緩存一段時間後會回收,若線程數不夠,則建立線程。
  3. Executors.newSingleThreadExecutor:建立單個線程數的線程池,它可以保證先進先出的執行順序。
  4. Executors.newScheduledThreadPool:建立一個可以執行延遲任務的線程池。
  5. Executors.newSingleThreadScheduledExecutor:建立一個單線程的可以執行延遲任務的線程池。
  6. Executors.newWorkStealingPool:建立一個搶占式執行的線程池(任務執行順序不确定)【JDK 1.8 添加】。
  7. ThreadPoolExecutor:手動建立線程池的方式,它建立時最多可以設定 7 個參數。

接下來我們分别來看這 7 種線程池的具體使用。

1.FixedThreadPool

建立一個固定大小的線程池,可控制并發線程數。

使用 FixedThreadPool 建立 2 個固定大小的線程池,具體實作代碼如下:

public static void fixedThreadPool() {
    // 建立 2 個線程的線程池
    ExecutorService threadPool = Executors.newFixedThreadPool(2);

    // 建立任務
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("任務被執行,線程:" + Thread.currentThread().getName());
        }
    };

    // 線程池執行任務(一次添加 4 個任務)
    // 執行任務的方法有兩種:submit 和 execute
    threadPool.submit(runnable);  // 執行方式 1:submit
    threadPool.execute(runnable); // 執行方式 2:execute
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}           

以上程式的執行結果如下圖所示:

面試突擊28:線程池有幾種建立方式?推薦使用哪種?

如果覺得以上方法比較繁瑣,還用使用以下簡單的方式來實作線程池的建立和使用:

public static void fixedThreadPool() {
    // 建立線程池
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    // 執行任務
    threadPool.execute(() -> {
        System.out.println("任務被執行,線程:" + Thread.currentThread().getName());
    });
}           

2.CachedThreadPool

建立一個可緩存的線程池,若線程數超過任務所需,那麼多餘的線程會被緩存一段時間後才被回收,若線程數不夠,則會建立線程。

CachedThreadPool 使用示例如下:

public static void cachedThreadPool() {
    // 建立線程池
    ExecutorService threadPool = Executors.newCachedThreadPool();
    // 執行任務
    for (int i = 0; i < 10; i++) {
        threadPool.execute(() -> {
            System.out.println("任務被執行,線程:" + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    }
}           
面試突擊28:線程池有幾種建立方式?推薦使用哪種?

從上述結果可以看出,線程池建立了 10 個線程來執行相應的任務。

使用場景

CachedThreadPool 是根據短時間的任務量來決定建立的線程數量的,是以它适合短時間内有突發大量任務的處理場景。

3.SingleThreadExecutor

建立單個線程的線程池,它可以保證先進先出的執行順序。

SingleThreadExecutor 使用示例如下:

public static void singleThreadExecutor() {
    // 建立線程池
    ExecutorService threadPool = Executors.newSingleThreadExecutor();
    // 執行任務
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + ":任務被執行");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    }
}           
面試突擊28:線程池有幾種建立方式?推薦使用哪種?

單個線程的線程池有什麼意義?

單個線程的線程池相比于線程來說,它的優點有以下 2 個:

  • 可以複用線程:即使是單個線程池,也可以複用線程。
  • 提供了任務管理功能:單個線程池也擁有任務隊列,在任務隊列可以存儲多個任務,這是線程無法實作的,并且當任務隊列滿了之後,可以執行拒絕政策,這些都是線程不具備的。

4.ScheduledThreadPool

建立一個可以執行延遲任務的線程池。

使用示例如下:

public static void scheduledThreadPool() {
    // 建立線程池
    ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
    // 添加定時執行任務(1s 後執行)
    System.out.println("添加任務,時間:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任務被執行,時間:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 1, TimeUnit.SECONDS);
}           
面試突擊28:線程池有幾種建立方式?推薦使用哪種?

從上述結果可以看出,任務在 1 秒之後被執行了,實作了延遲 1s 再執行任務。

5.SingleThreadScheduledExecutor

建立一個單線程的可以執行延遲任務的線程池,此線程池可以看作是 ScheduledThreadPool 的單線程池版本。

它的使用示例如下:

public static void SingleThreadScheduledExecutor() {
    // 建立線程池
    ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
    // 添加定時執行任務(2s 後執行)
    System.out.println("添加任務,時間:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任務被執行,時間:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 2, TimeUnit.SECONDS);
}           
面試突擊28:線程池有幾種建立方式?推薦使用哪種?

從上述結果可以看出,任務在 2 秒之後被執行了。

6.newWorkStealingPool

建立一個搶占式執行的線程池(任務執行順序不确定),此方法是 JDK 1.8 版本新增的,是以隻有在 JDK 1.8 以上的程式中才能使用。

newWorkStealingPool 使用示例如下:

public static void workStealingPool() {
    // 建立線程池
    ExecutorService threadPool = Executors.newWorkStealingPool();
    // 執行任務
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被執行,線程名:" + Thread.currentThread().getName());
        });
    }
    // 確定任務執行完成
    while (!threadPool.isTerminated()) {
    }
}           
面試突擊28:線程池有幾種建立方式?推薦使用哪種?

從上述結果可以看出,任務的執行順序是不确定的,因為它是搶占式執行的。

7.ThreadPoolExecutor

ThreadPoolExecutor 是最原始、也是最推薦的手動建立線程池的方式,它在建立時最多提供 7 個參數可供設定。

ThreadPoolExecutor 使用示例如下:

public static void myThreadPoolExecutor() {
    // 建立線程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
    // 執行任務
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被執行,線程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}           
面試突擊28:線程池有幾種建立方式?推薦使用哪種?

ThreadPoolExecutor 相比于其他建立線程池的優勢在于,它可以通過參數來控制最大任務數和拒絕政策,讓線程池的執行更加透明和可控,是以在阿裡巴巴《Java開發手冊》是這樣規定的:

【強制要求】線程池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明确線程池的運作規則,規避資源耗盡的風險。

說明:Executors 傳回的線程池對象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,進而導緻 OOM。

2)CachedThreadPool:允許的建立線程數量為 Integer.MAX_VALUE,可能會建立大量的線程,進而導緻 OOM。

總結

線程池的建立方式總共有以下 7 種:

而線程池的建立推薦使用最後一種 ThreadPoolExecutor 的方式來建立,因為使用它可以明确線程池的運作規則,規避資源耗盡的風險。

繼續閱讀