線程池ThreadPoolExecutor的理解與使用
前幾天客戶提了新需求,需要寫一個服務並且利用多線程去處理。雖然以前有這種例子但是是方法級的,這裏寫一個全局的來使用。這幾天都在測試今天有空把了解的東西都寫下來希望以後再用到直接找自己的博客就可以了。希望做自己的百度。
這幾天心得如下:
1.瞭解線程池:
有摘抄百度的東西:
1.1構造方法:
JAVA中自帶的線程池類爲 java.util.concurrent.ThreadPoolExecutor
常用首選構造方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
如果沒有特別需求,一般都是這個構造。
1.2參數:
corePoolSize: 線程池維護線程的最少數量:核心線程數,會一直存活,即使沒有任務,線程池也會維護線程的最少數量
當核心線程被用完以後後來的任務會被加入隊列中。
maximumPoolSize:線程池維護線程的最大數量
當隊列滿的時候會在開新的線程執行任務,直到達到最大線程數,後面的任務則使用策略。
keepAliveTime: 線程池維護線程所允許的空閒時間:線程池維護線程所允許的空閒時間,當線程空閒時間達到keepAliveTime,該線程會退出,直到線程數量等於corePoolSize。如果allowCoreThreadTimeout設置爲true,則所有線程均會退出直到線程數量爲0。(不常用到這個服務的童鞋可以這樣設置)
unit: 線程池維護線程所允許的空閒時間的單位:可選參數值爲:TimeUnit中的幾個靜態屬性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。(一般用毫秒或秒)
workQueue: 線程池所使用的緩衝隊列:線程池所使用的緩衝隊列,常用的是:java.util.concurrent.ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
handler: 線程池對拒絕任務的處理策略: 線程池中的數量大於maximumPoolSize,對拒絕任務的處理策略,默認值ThreadPoolExecutor.AbortPolicy()。
RejectedExecutionHandler 默認有四個選擇:
ThreadPoolExecutor.AbortPolicy()當線程池中的數量等於最大線程數時、直接拋出拋出java.util.concurrent.RejectedExecutionException異常。
ThreadPoolExecutor.CallerRunsPolicy() 當線程池中的數量等於最大線程數時、重試執行當前的任務,交由調用者線程來執行任務(我的服務用的是這個)。
ThreadPoolExecutor.DiscardOldestPolicy() 當線程池中的數量等於最大線程數時、拋棄線程池中最後一個要執行的任務,並執行新傳入的任務(項目中以前都用的這個策略)。
ThreadPoolExecutor.DiscardPolicy() 當線程池中的數量等於最大線程數時,不做任何動作。
shutdown() 當前空閒的線程interrupt掉,正在執行的會等執行完成。
不shutdown()等線程池任務執行完以後只會留核心線程數,其他的也會停止。
方法級線程池使用,全局的我沒使用。
隊列有三種通用策略(摘抄自百度):
直接提交。工作隊列的默認選項是 SynchronousQueue,它將任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界 maximumPoolSizes 以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。
無界隊列。使用無界隊列(例如,不具有預定義容量的 LinkedBlockingQueue)將導致在所有 corePoolSize 線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過 corePoolSize。(因此,maximumPoolSize 的值也就無效了。)當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列;例如,在 Web 頁服務器中。這種排隊可用於處理瞬態突發請求,當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。
有界隊列。當使用有限的 maximumPoolSizes 時,有界隊列(如 ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度地降低 CPU 使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞(例如,如果它們是 I/O 邊界),則系統可能爲超過您許可的更多線程安排時間。使用小型隊列通常要求較大的池大小,CPU 使用率較高,但是可能遇到不可接受的調度開銷,這樣也會降低吞吐量。
我認爲項目裏不可能使用無界隊列,有資源耗盡風險。
使用有界隊列,需要考量資源分配,少線程大隊列(我目前使用這種方案,不過根據業務人員的操作頻率與數量級分析,想改爲小隊列線程數大,這還需要考慮服務器硬件水平),或者大數量線程小隊列。
線程多開得多耗得資源也多。
1.3 線程的執行:
截圖貌似要上傳圖片太麻煩了,直接考了代碼來(項目使用JDK1.6):
執行:
一個任務通過execute(Runnable)方法添加進線程池,被線程池中的線程執行,執行的就是
Runnable中的run()方法。
這時:execute方法先判斷空指針,然後判斷當前線程數量是否大於核心線程數,如果小於則調用addIfUnderCorePoolSize。
再次判斷如果線程池的狀態不爲運行狀態或當前線程池數爲0,則調用ensureQueuedTaskHandled方法
判斷線程池運行,如果狀態不爲運行狀態,從workQueue中刪除, 並調用reject做拒絕處理。
如線程池workQueue offer失敗或不處於運行狀態,調用addIfUnderMaximumPoolSize,addIfUnderMaximumPoolSize方法基本和addIfUnderCorePoolSize實現類似,不同點在於根據最大線程數(maximumPoolSize)進行比較,如果超過最大線程數,返回false,調用reject方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();//空指針
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
addIfUnderCorePoolSize首先加鎖,防併發,再次判斷當前線程數小於corePoolSize並且線程池處於RUNNING狀態,則調用addThread增加線程
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < corePoolSize && runState == RUNNING)
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
addThread方法首先創建Work對象,然後調用threadFactory創建新的線程,
如果創建的線程不爲null,將Work對象的thread屬性設置爲此創建出來的線程,
並將此Work對象放入workers中,然後在增加當前線程池的中線程數,
增加後回到addIfUnderCorePoolSize方法 ,釋放mainLock,最後啓動這個新創建的線程來執行新傳入的任務。
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w);
if (t != null) {
w.thread = t;
workers.add(w);
int nt = ++poolSize;
if (nt > largestPoolSize)
largestPoolSize = nt;
}
return t;
}
addIfUnderCorePoolSize中最後t.start();執行Worker的run方法先執行傳入的任務然後一直循環獲取新的任務並執行。
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
通過WorkQueue的poll或task方法來獲取下一個要執行的任務。
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut)//則通過poll取任務,若等待一定的時間取不到任務,則返回null
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) {
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers();
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
}
然後也就是判斷一下:說如果線程池處於STOP狀態、或者任務隊列已爲空或者允許爲核心池線程設置空閒存活時間並且線程數大於1時,允許worker結束。如果允許worker退出,則調用interruptIdleWorkers()中斷處於空閒狀態的worker。
以上總結爲:
當添加任務進行處理時:
1.當前線程數量小於核心線程數量,並處於running狀態,創建添加新任務給新的線程。
2.如果當前線程池中的數量等於corePoolSize,併線程池處於Running狀態,緩衝隊列 workQueue未滿,那麼任務被放入緩衝隊列、等待任務調度執行。
3.如果當前線程池中的數量大於corePoolSize,緩衝隊列workQueue已滿,並且線程池中的數量小於maximumPoolSize,新提交任務會創建新線程執行任務。
4.如果當前線程池中的數量大於corePoolSize,緩衝隊列workQueue已滿,並且線程池中的數量等於maximumPoolSize,新提交任務由Handler處理。
5.當線程池中的線程大於corePoolSize時,多餘線程空閒時間超過keepAliveTime時,會關閉這部分線程。
2.構造線程池:
沒什麼驚豔(經驗??)的寫法DEMO我就不寫了,直接寫發佈後的:
Spring初始化傳入參數,並調用init方法
<!-- 線程池初始化 -->
<bean id="ThreadPoolBean" class="com.sinotrans.bms.webServlet.bean.ThreadPoolBean" init-method="initThreadPool">
<property name="COREPOOLSIZE">
<value type="java.lang.Integer">8</value>
</property>
<property name="MAXIMUMPOOLSIZE">
<value type="java.lang.Integer">16</value>
</property>
<property name="KEEPALIVETIME">
<value type="java.lang.Long">30</value>
</property>
</bean>
package com.sinotrans.bms.webServlet.bean;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 初始化線程池
* @Date 2016年12月05日
*/
public class ThreadPoolBean {
private static ThreadPoolExecutor threadPool = null;
private static int COREPOOLSIZE;
private static int MAXIMUMPOOLSIZE;
private static Long KEEPALIVETIME;
private final static ArrayBlockingQueue<Runnable> BLOCKINGQUEUE = new ArrayBlockingQueue<Runnable>(10000);
private ThreadPoolBean(){}//單利不要被NEW 來NEW去的 一會服務器炸了
/*
* 構造線程池
*/
public void initThreadPool(){
if(threadPool == null){//防止被多次初始化
//DiscardOldestPolicy設置策略爲擠掉最舊的 CallerRunsPolicy誰調用的任務誰執行,主線程調用主線程執行
ThreadPoolExecutor thread = new ThreadPoolExecutor(COREPOOLSIZE,
MAXIMUMPOOLSIZE, KEEPALIVETIME, TimeUnit.SECONDS,
BLOCKINGQUEUE, new ThreadPoolExecutor.CallerRunsPolicy());
threadPool = thread;
}
System.out.println("Thread pool init success.");
}
/*
* 或得線程池對象
*/
public static ThreadPoolExecutor getThreadPool(){
return threadPool;
}
public void setCOREPOOLSIZE(int corepoolsize) {
COREPOOLSIZE = corepoolsize;
}
public void setMAXIMUMPOOLSIZE(int maximumpoolsize) {
MAXIMUMPOOLSIZE = maximumpoolsize;
}
public void setKEEPALIVETIME(Long keepalivetime) {
KEEPALIVETIME = keepalivetime;
}
}
初始化方式很多,也可以使用Spring調用構造方法來初始化。
Servlet加入WEB.xml調用servlet的時候初始化。
或者使用攔截器,都可以。比較偏向於Spring。
3.使用線程池:
ThreadPoolExecutor threadPool = ThreadPoolBean.getThreadPool();
InvokingOfNewThreadManager iontModel = (InvokingOfNewThreadManager)BaseServlet.getBean("invokingOfNewThreadManager");//我這是寫在servlet裏的一個服務所以需要從Spring拿注入的bean
iontModel.setBatchNumber(batchNumber);
iontModel.setFlag(false);//發票爲true
iontModel.setIsAudit(isAudut);//補收補付審覈
iontModel.setEscoCompanyNo(escoCompanyNo);
iontModel.setProName("CG_LOAD_REAL_TIME_EFEP_A");
threadPool.execute(iontModel);
System.out.println(threadPool.getPoolSize()+"/"+threadPool.getQueue().size());//打印線程數與隊列大小
我的寫法就是mananger繼承runnable使之有事物:
@Service
public class InvokingOfNewThreadManagerImpl extends BaseManagerImpl implements
Runnable, InvokingOfNewThreadManager {
@Autowired
private EoInformationMainManager eoInformationMainManager;
//屬性都是業務需要的
private Long batchNumber;
private boolean flag;
private String proName;
private String isAudit;
private String escoCompanyNo;
public void run() {
//業務邏輯
try {//模擬業務邏輯的處理
Thread.sleep(30000);
System.out.println("test");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
IgnoreLoginController.virtualLogout();
}
public void setBatchNumber(Long batchNumber) {
this.batchNumber = batchNumber;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void setProName(String proName) {
this.proName = proName;
}
public void setIsAudit(String isAudit) {
this.isAudit = isAudit;
}
public void setEscoCompanyNo(String escoCompanyNo) {
this.escoCompanyNo = escoCompanyNo;
}
當然了,我們這個服務不會和搶票一樣的併發量,到併發高的時候也就幾百併發每個調用幾萬數據量。目前可以抗住6000併發多測無益(業務就沒那麼多併發)就沒再測試,後期會提升數據量進行測試。
畢設,或者demo可以直接用,並不用修改什麼。
還是會繼續把筆記寫進來。爭取做自己的百度。