線程池的介紹
不用線程池存在的問題
- 反覆創建銷燬線程開銷大
- 過多的線程佔用大量內存
線程池的使用,就類似於計劃經濟,控制資源總量,複用線程。有如下3點好處。
- 加快響應速度。消除了線程創建帶來的延時。
- 合理的利用CPU和內存。控制線程的數量,既不會因爲線程太多導致內存溢出,也不會因爲太少導致CPU資源的浪費。
- 便於統一管理線程。例如數據統計。
線程的使用場景舉例:
- 服務器接收大量請求,使用線程池可以大大減少線程創建和銷燬次數,提高服務器工作效率。
- 開發中使用多個線程時,可以考慮採用線程池。
線程池結構
ThreadPoolExecutor繼承AbstractExecutorService(實現ExecutorService),4個類/接口都可以視作線程池,並向上轉型(多態)。ThreadPoolExecutor中有5個嵌套類,其中4個爲RejectedExecutionHandler拒絕策略接口的實現類,1個是Worker類,用於維護正在運行任務的線程的中斷控制狀態,以及其他的次要信息。
Executors是線程池工具類,可以通過靜態工廠方法快捷實現線程池,例如Executors.newFixedThreadPool(10).
舉個麪包店的例子
假設有一所麪包店,麪包店平時有5位麪包師,每天隨着訂單增加,5位師傅逐漸加入到工作中去。不能及時處理的訂單,會掛在牆上的訂單欄上,等待麪包師順序處理。
訂單過多時,5個麪包師忙不過來就招臨時麪包師,最多5名。根據任務多少會調整臨時麪包師的數量,太長時間沒事幹的臨時麪包師會被辭退掉。
店鋪關閉或者訂單在麪包師達到10名後,便不再接新單。
線程池構造器
ThreadPoolExecutor只有四個構造器,最多有7個參數。其中前5個是必要參數,線程工廠和拒絕策略可選。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
線程添加策略
- 如果線程數小於corePoolSize,即使其他工作線程處於空閒狀態,仍會創建新線程運行新任務。【疑問:是一下全建出來,還是一個一個創建】
- 如果線程數大於corePoolSize但是少於maxPoolSize,則將任務放入到阻塞隊列中。
- 如果隊列已滿,並且線程數小於maxPoolSize,則創建一個新線程運行任務。
- 如果隊列已滿,並且線程數大於或等於maxPoolSize,則拒絕該任務。
增減線程的特點
- 如果設置相同的corePoolSize和maxThreadPoolSize,相當於創建固定大小的線程池。此時,工作隊列通常選用無界隊列,超時時間爲0L。
- 線程池希望保持較少的線程數,只在負載變得很大的時候才增加新的線程。
- 通過設置maxPoolSize爲Integer.MAX_VALUE,可以允許線程池容納任意容量的併發任務。
- 只有在隊列填滿時,纔會創建多於corePoolSize的新線程。如果採用無界隊列,例如LinkedBlockingQueue,線程數不會超過corePoolSize。
線程池的拒絕策略
拒絕時機–參見execute方法中兩個調用位置
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
- 當Executor關閉時,提交新任務會被拒絕;
- 當Executor對最大線程數和工作隊列容量使用有限邊界,並且已經飽和時。
拒絕策略
- AbortPolicy:直接拋出異常,提示沒有提交成功
- DiscardPolicy:直接丟棄新任務,不拋出異常。
- DiscardOldestPolicy:丟棄最老的任務
- CallerRunsPolicy:讓提交任務的線程執行。好處是避免了業務損失;提交速度降低(主線程一直提交任務,線程池和工作隊列滿後,主線程開始執行提交的任務,相當於給了線程池一個緩衝的時間)。
源碼分析
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
拒絕策略接口只有一個方法,目的很明確。
4種拒絕策略實現RejectedExecutionHandler接口,代碼實現也很簡單清楚,此處只介紹DiscardOldestPolicy和CallerRunsPolicy兩種策略。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 1. 如果線程池是RUNNING狀態,並且線程達到飽和,則丟棄最老的任務,重新執行execute方法
// 2. 如果線程池非RUNNING狀態,則直接丟棄任務。
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
// 1. 如果線程池是RUNNING狀態,並且線程達到飽和,則使用調用者線程執行任務
// 2. 如果線程池非RUNNING狀態,則直接丟棄任務。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
我們知道直接調用runnable的run()方法是同步調用,調用者線程也就是調用當前線程池execute方法的那個線程。
工作隊列
工作隊列通用策略有3種。
- 直接提交隊列:默認爲SynchronousQueue,直接將任務提交給線程,不保持他們。如果不存在空閒的線程,則試圖將任務加入隊列將失敗,會新創建一個線程。通常要求無界
maximumPoolSizes 。 - 無界隊列:例如LinkedBlockingQueue,用於突發請求。
- 有界隊列:例如ArrayBlockingQueue,有利於防止資源耗盡,較難調整隊列大小和最大池大小的值。
線程工廠
public interface ThreadFactory {
Thread newThread(Runnable r);
}
ThreadFactory只有一個方法,是一個函數式接口。
static class DefaultThreadFactory implements ThreadFactory {
// 標記線程池的數量
private static final AtomicInteger poolNumber = new AtomicInteger(1);
// 新線程所在線程組
private final ThreadGroup group;
// 標記線程的數量
private final AtomicInteger threadNumber = new AtomicInteger(1);
// 線程的命名前綴(所在線程池)
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
// 策略線程組或創建線程工廠的線程所在線程組
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
// 只生產用戶線程
if (t.isDaemon())
t.setDaemon(false);
// 線程的安全級別是5
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
常見的線程池
Executors快捷生成的線程池參數總結。
默認的線程工廠ThreadFactory是Executors.defaultThreadFactory(),拒絕策略是AbortPolicy。
- SingleThreadPool:內部和FixedThreadPool基本一致,只是線程數不同。
- CachedThreadPool:創建可緩存線程池。是無界線程池,具有自動回收多餘線程的功能。採用的是同步移交隊列,任務直接給到空閒線程或者創建新線程執行。如果線程空閒(沒有任務執行)60秒,回收線程。
- ScheduledThreadPool:支持定時及週期性任務執行的線程池。
- WorkStealingPool是JDK8中新增的一種線程池,是新的線程池類ForkJoinPool的擴展,能夠合理地使用CPU對任務做並行操作,適合耗時的場景,例如遞歸、分而治之,並且不加鎖的場景。
線程池的注意點
避免任務堆積,避免線程數過度增加,排查線程泄漏。線程多時,有可能存在線程泄漏,線程執行完畢但是沒被回收,可能是任務邏輯問題。