玩轉 Java 線程池(1):線程池到底應該怎麼建?

0 創建線程池的核心問題

根據阿里巴巴的《Java開發規範》裏的一條規定,
在這裏插入圖片描述
這條規定指出了,當我們想使用線程池的時候,最好不要偷懶,最好要自己手動創建線程池,那麼問題就來了,手動創建線程池到底要如何去創建?

1 我的核心線程數量到底應該創建多少?

1.1 我們設置合適的線程數量是爲了什麼?

爲了榨乾硬件的性能,我們知道,一個程序在服務器上去除網絡傳輸的時間,剩下的就是『計算』和『I/O』的時間了。這裏的『I/O』既包括和 主存和輔存 交換數據的時間,也包括網絡數據傳輸到服務器,服務器拷貝的內核空間。我們設置合適的線程數量就是爲了可以充分利用每個CPU,磁盤的交換數據的效率達到最大。

1.2 根據程序的類型分類討論

  1. 比如 計算100000個隨機數的加法,這個就是實打實的計算密集型的任務。
  2. 比如 文件上傳任務,這個就是典型的『I/O』密集型的任務。
  3. 還有第三種『I/O、 計算混合型任務』,也就是目前的大部分程序,都是屬於這種,兩種耗時的任務都有涉及。

我們逐個討論

  1. 如果是計算密集型的任務,那麼設置的線程數爲:服務器CPU數 + 1
    爲什麼?因爲如果是一個任務是計算密集型的,那麼最理想的情況就是,所有的CPU都跑滿,這樣每個CPU的資源都得到了充分的利用。至於爲什麼要需要在CPU的個數上+1,網上比較流行的解釋就是,考慮到即便是CPU密集型的任務,其執行線程也可能也有可能在某個時間因爲某個原因出現等待(比如說缺頁中斷等等)。
  2. 如果是IO密集型的任務,那麼最好的方式的就是計算IO和計算所花費的時間比。如果 CPU 計算和 I/O 操作的耗時是 1:2,那麼合適的線程就是3,至於爲什麼。。。這裏用下圖來說明
    極客時間——10 | Java線程(中):創建多少線程纔是合適的?
    圖片來自 :極客時間——10 | Java線程(中):創建多少線程纔是合適的?

所以,如果是一個CPU,那麼合適的線程數量就是

1 +(IO耗時 / CPU耗時)

不過現在都是多核的CPU,所以合適的設置的線程數量就是:

CPU數 * ( 當只有一個CPU合適的線程數量

當然,我們工作中很難每次都完美的統計到IO和計算所用的時間比,所以,很多前輩們根據自己的工作經驗,就有了一個比較通用的線程數量計算,對於I/O 密集型的應用,最佳線程數爲:2 * CPU 的核數 + 1
注意,這裏還有一個重點,就是:如果你線程數越多那麼切換線程的代價也就越多,所以這裏的核心線程數量設置爲 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 總結

如果你需要保證的你提交的任務不丟失,確認執行,那麼建議使用 策略 CallerRunsPolicyDiscardOldestPolicy,甚至使用在 3.2 中的這個自己實現的拒絕策略,如果你的任務不重要,保證自己的程序的穩定性比較重要,那麼就建議使用DiscardPolicyAbortPolicy

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章