優雅的java線程池

前言
線程池想必大家也都用過,JDK的Executors 也自帶一些線程池。但是不知道大家有沒有想過,如何纔是最優雅的方式去使用過線程池嗎? 生產環境要怎麼去配置自己的線程池纔是合理的呢?
今天週末,剛好有時間來總結一下自己所認爲的’優雅’, 如有問題歡迎大家指正。

線程池使用規則
要使用好線程池,那麼一定要遵循幾個規則:

線程個數大小的設置
線程池相關參數配置
利用Hook嵌入你的行爲
線程池的關閉
線程池配置相關
線程池大小的設置
這其實是一個面試的考點,很多面試官會問你線程池coreSize 的大小來考察你對於線程池的理解。
首先針對於這個問題,我們必須要明確我們的需求是計算密集型還是IO密集型,只有瞭解了這一點,我們才能更好的去設置線程池的數量進行限制。

1、計算密集型
顧名思義就是應用需要非常多的CPU計算資源,在多核CPU時代,我們要讓每一個CPU核心都參與計算,將CPU的性能充分利用起來,這樣纔算是沒有浪費服務器配置,如果在非常好的服務器配置上還運行着單線程程序那將是多麼重大的浪費。對於計算密集型的應用,完全是靠CPU的核數來工作,所以爲了讓它的優勢完全發揮出來,避免過多的線程上下文切換,比較理想方案是:

線程數 = CPU核數+1,也可以設置成CPU核數*2,但還要看JDK的版本以及CPU配置(服務器的CPU有超線程)。

一般設置CPU * 2即可。

2、IO密集型
我們現在做的開發大部分都是WEB應用,涉及到大量的網絡傳輸,不僅如此,與數據庫,與緩存間的交互也涉及到IO,一旦發生IO,線程就會處於等待狀態,當IO結束,數據準備好後,線程纔會繼續執行。因此從這裏可以發現,對於IO密集型的應用,我們可以多設置一些線程池中線程的數量,這樣就能讓在等待IO的這段時間內,線程可以去做其它事,提高併發處理效率。那麼這個線程池的數據量是不是可以隨便設置呢?當然不是的,請一定要記得,線程上下文切換是有代價的。目前總結了一套公式,對於IO密集型應用:
線程數 = CPU核心數/(1-阻塞係數) 這個阻塞係數一般爲0.8~0.9之間,也可以取0.8或者0.9。
套用公式,對於雙核CPU來說,它比較理想的線程數就是20,當然這都不是絕對的,需要根據實際情況以及實際業務來調整:final int poolSize = (int)(cpuCore/(1-0.9))

針對於阻塞係數,《Programming Concurrency on the JVM Mastering》即《Java 虛擬機併發編程》中有提到一句話:

對於阻塞係數,我們可以先試着猜測,抑或採用一些細嫩分析工具或java.lang.management API 來確定線程花在系統/IO操作上的時間與CPU密集任務所耗的時間比值。

線程池相關參數配置
說到這一點,我們只需要謹記一點,一定不要選擇沒有上限限制的配置項
這也是爲什麼不建議使用Executors 中創建線程的方法。
比如,Executors.newCachedThreadPool的設置與無界隊列的設置因爲某些不可預期的情況,線程池會出現系統異常,導致線程暴增的情況或者任務隊列不斷膨脹,內存耗盡導致系統崩潰和異常。 我們推薦使用自定義線程池來避免該問題,這也是在使用線程池規範的首要原則! 小心無大錯,千萬別過度自信!
可以看下Executors中四個創建線程池的方法:

//使用無界隊列
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

//線程池數量是無限的
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
其他的就不再列舉了,大家可以自行查閱源碼。

第二,合理設置線程數量、和線程空閒回收時間,根據具體的任務執行週期和時間去設定,避免頻繁的回收和創建,雖然我們使用線程池的目的是爲了提升系統性能和吞吐量,但是也要考慮下系統的穩定性,不然出現不可預期問題會很麻煩!
第三,根據實際場景,選擇適用於自己的拒絕策略。進行補償,不要亂用JDK支持的自動補償機制!儘量採用自定義的拒絕策略去進行兜底!
第四,線程池拒絕策略,自定義拒絕策略可以實現RejectedExecutionHandler接口。
JDK自帶的拒絕策略如下:
AbortPolicy:直接拋出異常阻止系統正常工作。
CallerRunsPolicy:只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。
DiscardOldestPolicy:丟棄最老的一個請求,嘗試再次提交當前任務。
DiscardPolicy:丟棄無法處理的任務,不給予任何處理。

利用Hook
利用Hook,留下線程池執行軌跡:
ThreadPoolExecutor提供了protected類型可以被覆蓋的鉤子方法,允許用戶在任務執行之前會執行之後做一些事情。我們可以通過它來實現比如初始化ThreadLocal、收集統計信息、如記錄日誌等操作。這類Hook如beforeExecute和afterExecute。另外還有一個Hook可以用來在任務被執行完的時候讓用戶插入邏輯,如rerminated 。
如果hook方法執行失敗,則內部的工作線程的執行將會失敗或被中斷。

我們可以使用beforeExecute和afterExecute來記錄線程之前前和後的一些運行情況,也可以直接把運行完成後的狀態記錄到ELK等日誌系統。

關閉線程池
內容當線程池不在被引用並且工作線程數爲0的時候,線程池將被終止。我們也可以調用shutdown來手動終止線程池。如果我們忘記調用shutdown,爲了讓線程資源被釋放,我們還可以使用keepAliveTime和allowCoreThreadTimeOut來達到目的!
當然,穩妥的方式是使用虛擬機Runtime.getRuntime().addShutdownHook方法,手工去調用線程池的關閉方法!

我們可以通過ThreadPoolExecutor來實現自定義線程池

ThreadPoolTaskExecutor是spring core包中的,而ThreadPoolExecutor是JDK中的JUC。ThreadPoolTaskExecutor是對ThreadPoolExecutor進行了封裝處理。

 

最後附上本人項目生產用法

ThreadPoolTaskConfig
package com.zcckj.common.db.config.thread;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;


@Configuration
@Slf4j
public class ThreadPoolTaskConfig {

	@Value("${task.core_pool_size}")
	private Integer corePoolSize;
	@Value("${task.max_pool_size}")
	private Integer maxPoolSize;
	@Value("${task.queue_capacity}")
	private Integer queueCapacity;
	@Value("${task.keep_alive_seconds}")
	private Integer keepAliveSeconds;


	@Bean(name="taskExecutor")
	public ThreadPoolTaskExecutor taskExecutor(){
		ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
		//線程池維護線程的最少數量
		poolTaskExecutor.setCorePoolSize(corePoolSize);
		//線程池維護線程的最大數量
		poolTaskExecutor.setMaxPoolSize(maxPoolSize);
		//線程池所使用的緩衝隊列
		poolTaskExecutor.setQueueCapacity(queueCapacity);
		//線程池維護線程所允許的空閒時間
		poolTaskExecutor.setKeepAliveSeconds(keepAliveSeconds);
		poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
		return poolTaskExecutor;
	}

}

使用


    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    …………

    taskExecutor.execute(() -> dosomething……);

 

本文引用自:https://blog.csdn.net/u012881584/article/details/85221635

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