0 創建線程池的核心問題
根據阿里巴巴的《Java開發規範》裏的一條規定,
這條規定指出了,當我們想使用線程池的時候,最好不要偷懶,最好要自己手動創建線程池,那麼問題就來了,手動創建線程池到底要如何去創建?
1 我的核心線程數量到底應該創建多少?
1.1 我們設置合適的線程數量是爲了什麼?
爲了榨乾硬件的性能,我們知道,一個程序在服務器上去除網絡傳輸的時間,剩下的就是『計算』和『I/O』的時間了。這裏的『I/O』既包括和 主存和輔存 交換數據的時間,也包括網絡數據傳輸到服務器,服務器拷貝的內核空間。我們設置合適的線程數量就是爲了可以充分利用每個CPU,磁盤的交換數據的效率達到最大。
1.2 根據程序的類型分類討論
- 比如 計算100000個隨機數的加法,這個就是實打實的計算密集型的任務。
- 比如 文件上傳任務,這個就是典型的『I/O』密集型的任務。
- 還有第三種『I/O、 計算混合型任務』,也就是目前的大部分程序,都是屬於這種,兩種耗時的任務都有涉及。
我們逐個討論
- 如果是計算密集型的任務,那麼設置的線程數爲:服務器CPU數 + 1。
爲什麼?因爲如果是一個任務是計算密集型的,那麼最理想的情況就是,所有的CPU都跑滿,這樣每個CPU的資源都得到了充分的利用。至於爲什麼要需要在CPU的個數上+1,網上比較流行的解釋就是,考慮到即便是CPU密集型的任務,其執行線程也可能也有可能在某個時間因爲某個原因出現等待(比如說缺頁中斷等等)。 - 如果是IO密集型的任務,那麼最好的方式的就是計算IO和計算所花費的時間比。如果 CPU 計算和 I/O 操作的耗時是 1:2,那麼合適的線程就是3,至於爲什麼。。。這裏用下圖來說明
圖片來自 :極客時間——10 | Java線程(中):創建多少線程纔是合適的?
所以,如果是一個CPU,那麼合適的線程數量就是:
1 +(IO耗時 / CPU耗時)
不過現在都是多核的CPU,所以合適的設置的線程數量就是:
CPU數 * ( 當只有一個CPU合適的線程數量 )
當然,我們工作中很難每次都完美的統計到IO和計算所用的時間比,所以,很多前輩們根據自己的工作經驗,就有了一個比較通用的線程數量計算,對於I/O 密集型的應用,最佳線程數爲:2 * CPU 的核數 + 1。
注意,這裏還有一個重點,就是:如果你線程數越多那麼切換線程的代價也就越多,所以這裏的核心線程數量設置爲 1, 這樣,可以保證,在任務提交的過程中,我們可以保證使用少量的線程的情況下完成任務。
- 如果是混合型任務,那麼,我們就把任務拆分成計算型任務和IO型任務,將這些子任務提交給各自的類型的線程池執行就行。
2 用默認的創建線程工廠還是自己實現?
阿里巴巴的《Java開發規範》中有規定:
所以這裏重點就是,你需要給創建的線程有意義的名字,這裏就直接規定了,你不能使用默認方法來創建線程了。那麼我們要怎麼創建有意義的線程名稱的線程?目前有兩種主流的方法。
2.1 用Guava的來創建
@Slf4j
public class ThreadPoolExecutorDemo00 {
public static void main(String[] args) {
ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder()
.setNameFormat("我的線程 %d");
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
10,
60,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(100),
threadFactoryBuilder.build());
IntStream.rangeClosed(1, 100)
.forEach(i -> {
executor.submit(() -> {
log.info("id: {}", i);
});
});
executor.shutdown();
}
}
來看看效果:
2.2 自己實現ThreadFactory
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
10,
60,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(100),
new MyNewThreadFactory("我的線程池"));
IntStream.rangeClosed(1, 100)
.forEach(i -> {
executor.submit(() -> {
log.info("id: {}", i);
});
});
executor.shutdown();
}
public static class MyNewThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final String namePrefix;
MyNewThreadFactory(String whatFeatureOfGroup) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "From MineNewThreadFactory-" + whatFeatureOfGroup + "-worker-thread-";
}
@Override
public Thread newThread(Runnable r) {
String name = namePrefix + poolNumber.getAndIncrement();
Thread thread = new Thread(group, r,
name,
0);
if (thread.isDaemon()) {
thread.setDaemon(false);
}
if (thread.getPriority() != Thread.NORM_PRIORITY) {
thread.setPriority(Thread.NORM_PRIORITY);
}
return thread;
}
}
來看看效果:
3 拒絕策略到底用哪個?
3.1 先來看看四個基本的拒絕策略
-
(1) CallerRunsPolicy :任務不給線程池執行,給提交任務的主線程執行,看代碼:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } }
-
(2) AbortPolicy :直接扔出異常,以及拒絕掉任務,同時這個也是默認的拒絕策略 ,也就是說,如果你在創建線程池的時候不設置拒絕策略的話,那麼默認的拒絕策略就是這個。直接看代碼:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); }
-
(3) DiscardPolicy:單純的拒絕,別的啥也不做,看代碼:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }
-
(4) DiscardOldestPolicy:把最老的任務拋棄,然把當前的任務放入阻塞隊列中這個其實很好理解,直接看源碼理解最好理解了
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } }
小總結一波,可以看出(1)、(4) 的拒絕策略,雖然稱之爲拒絕了,但是仍然會執行任務。但是(2)、(3) 就會直接拒絕任務,使得任務出現丟失。
3.2 自己實現拒絕策略
比如我們想要實現一個拒絕策略,想要我們提交的任務最終可以提交到隊列中,採用阻塞等待的策略來完成,那麼我們要怎麼寫代碼?其實根據JDK的代碼,我們可以寫出自己的拒絕策略,首先要實現 RejectedExecutionHandler
這個接口。
public static class EnqueueByBlockingPolicy implements RejectedExecutionHandler {
public EnqueueByBlockingPolicy() { }
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (e.isShutdown()) {
return;
}
try {
e.getQueue().put(r);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
}
當然在工作項目中不不建議你這麼寫,因爲這樣拒絕策略會導致主線程阻塞,而且沒有設置超時退出。如果你真要用這樣方式的拒絕,最好使用 BlockingQueue#offer(E, long, TimeUnit)
這個方法,這樣畢竟有超時退出。但是使用 offer
不能保證你肯定會提交任務到隊列;具體拒絕策略的代碼,要結合實際需求。
3.3 總結
如果你需要保證的你提交的任務不丟失,確認執行,那麼建議使用 策略 CallerRunsPolicy
, DiscardOldestPolicy
,甚至使用在 3.2 中的這個自己實現的拒絕策略,如果你的任務不重要,保證自己的程序的穩定性比較重要,那麼就建議使用DiscardPolicy
,AbortPolicy
。