什么是線程池?
線程池就是創建若干個可執行的線程放入一個池(容器)中,有任務需要處理時,會提交到線程池中的任務隊列,處理完之后線程並不會被銷毀,而是仍然在線程池中等待下一個任務。
為什么要使用線程池?
因為 Java 中創建一個線程,需要調用操作系統內核的 API,操作系統要為線程分配一系列的資源,成本很高,所以線程是一個重量級的對象,應該避免頻繁創建和銷毀。
使用線程池就能很好地避免頻繁創建和銷毀。
線程池是一種生產者——消費者模式
先看下一個簡單的 Java 線程池的代碼
package constxiong.concurrency.a010; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; /** * 簡單的線程池 * @author ConstXiong */ public class ThreadPool { //阻塞隊列實現生產者-消費者 BlockingQueue<Runnable> taskQueue; //工作線程集合 List<Thread> threads = new ArrayList<Thread>(); //線程池的構造方法 ThreadPool(int poolSize, BlockingQueue<Runnable> taskQueue) { this.taskQueue = taskQueue; //啟動線程池對應 size 的工作線程 for (int i = 0; i < poolSize; i++) { Thread t = new Thread(() -> { while (true) { Runnable task; try { task = taskQueue.take();//獲取任務隊列中的下一個任務 task.run(); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); threads.add(t); } } //提交執行任務 void execute(Runnable task) { try { //把任務方法放到任務隊列 taskQueue.put(task); } catch (InterruptedException e) { e.printStackTrace(); } } }
線程池的使用測試
package constxiong.concurrency.a010; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * 測試線程池的使用 * @author ConstXiong */ public class TestThreadPool { public static void main(String[] args) { // 創建有界阻塞任務隊列 BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(10); // 創建 3個 工作線程的線程池 ThreadPool tp = new ThreadPool(3, taskQueue); //提交 10 個任務 for (int i = 1; i <= 10; i++) { final int j = i; tp.execute(() -> { System.out.println("執行任務" + j); }); } } }
打印結果
執行任務1
執行任務2
執行任務3
執行任務6
執行任務5
執行任務4
執行任務8
執行任務7
執行任務10
執行任務9
這個線程池的代碼中
- poolSize 是線程池工作線程的個數
- BlockingQueue taskQueue 是用有界阻塞隊列存儲 Runnable 任務
- execute(Runnable task) 提交任務
- 線程池對象被創建,就自動啟動 poolSize 個工作線程
- 工作線程一直從任務隊列 taskQueue 中取任務
線程池的原理就是這么簡單,但是 JDK 中的線程池的功能,要遠比這個強大的多。
JDK 中線程池的使用
JDK 中提供的最核心的線程池工具類 ThreadPoolExecutor,在 JDK 1.8 中這個類最復雜的構造方法有 7 個參數。
ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- corePoolSize:線程池保有的最小線程數。
- maximumPoolSize:線程池創建的最大線程數。
- keepAliveTime:上面提到項目根據忙閑來增減人員,那在編程世界里,如何定義忙和閑呢?很簡單,一個線程如果在一段時間內,都沒有執行任務,說明很閑,keepAliveTime 和 unit 就是用來定義這個“一段時間”的參數。也就是說,如果一個線程空閑了keepAliveTime & unit這么久,而且線程池的線程數大於 corePoolSize ,那么這個空閑的線程就要被回收了。
- unit:keepAliveTime 的時間單位
- workQueue:任務隊列
- threadFactory:線程工廠對象,可以自定義如何創建線程,如給線程指定name。
- handler:自定義任務的拒絕策略。線程池中所有線程都在忙碌,且任務隊列已滿,線程池就會拒絕接收再提交的任務。handler 就是拒絕策略,包括 4 種(即RejectedExecutionHandler 接口的 4個實現類)。
- AbortPolicy:默認的拒絕策略,throws RejectedExecutionException
- CallerRunsPolicy:提交任務的線程自己去執行該任務
- DiscardPolicy:直接丟棄任務,不拋出任何異常
- DiscardOldestPolicy:丟棄最老的任務,加入新的任務
JDK 的並發工具包里還有一個靜態線程池工廠類 Executors,可以方便地創建線程池,但是由於 Executors 創建的線程池內部很多地方用到了無界任務隊列,在高並發場景下,無界任務隊列會接收過多的任務對象,導致 JVM 拋出OutOfMemoryError,整個 JVM 服務崩潰,影響嚴重。所以很多公司,尤其互聯網大廠已經不建議使用 Executors 去創建線程。
Executors 的簡介
雖然不建議使用,作為對 JDK 的學習,還是簡單介紹一下.
- newFixedThreadPool:創建定長線程池,每當提交一個任務就創建一個線程,直到達到線程池的最大數量,這時線程數量不再變化,當線程發生錯誤結束時,線程池會補充一個新的線程
- newCachedThreadPool:創建可緩存的線程池,如果線程池的容量超過了任務數,自動回收空閑線程,任務增加時可以自動添加新線程,線程池的容量不限制
- newScheduledThreadPool:創建定長線程池,可執行周期性的任務
- newSingleThreadExecutor:創建單線程的線程池,線程異常結束,會創建一個新的線程,能確保任務按提交順序執行
- newSingleThreadScheduledExecutor:創建單線程可執行周期性任務的線程池
- newWorkStealingPool:任務可竊取線程池,不保證執行順序,當有空閑線程時會從其他任務隊列竊取任務執行,適合任務耗時差異較大。