寫在前面
本文是以同tomcat 7.0.57。 jdk版本1.7.0_80爲例。
線程池在tomcat中的創建實現爲:
public abstract class AbstractEndpoint<S> { public void createExecutor() { internalExecutor = true; TaskQueue taskqueue = new TaskQueue(); TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority()); executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf); taskqueue.setParent( (ThreadPoolExecutor) executor); } }
同時(重點):tomcat的線程池擴展了jdk的executor,而且隊列用的是自己的task queue,因此其策略與jdk的有所不同,需要注意一下。
tomcat線程池策略
場景1:接受一個請求,此時tomcat啓動的線程數還沒有達到corePoolSize(tomcat裏頭叫minSpareThreads),tomcat會啓動一個線程來處理該請求;
場景2:接受一個請求,此時tomcat啓動的線程數已經達到了corePoolSize,tomcat把該請求放入隊列(offer
),如果放入隊列成功,則返回,放入隊列不成功,則嘗試增加工作線程,在當前線程個數<maxThreads的時候,可以繼續增加線程來處理,超過maxThreads的時候,則繼續往等待隊列裏頭放,等待隊列放不進去,則拋出RejectedExecutionException;
值得注意的是,使用LinkedBlockingQueue的話,默認是使用Integer.MAX_VALUE,即無界隊列(這種情況下如果沒有配置隊列的capacity的話,隊列始終不會滿,那麼始終無法進入開啓新線程到達maxThreads個數的地步,則此時配置maxThreads其實是沒有意義的)。
而TaskQueue的隊列capacity爲maxQueueSize,默認也是Integer.MAX_VALUE。但是,其重寫offer方法,當其線程池大小小於maximumPoolSize的時候,返回false,即在一定程度改寫了隊列滿的邏輯,修復了使用LinkedBlockingQueue默認的capacity爲Integer.MAX_VALUE的時候,maxThreads失效的"bug"。從而可以繼續增長線程到maxThreads,超過之後,繼續放入隊列。
tomcat的線程池使用了自己擴展的taskQueue,而不是Executors工廠方法裏頭用的LinkedBlockingQueue。(主要是修改了offer的邏輯
)TaskQueue實現的offer操作如下:
package org.apache.tomcat.util.threads; import java.util.Collection; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; public class TaskQueue extends LinkedBlockingQueue<Runnable> { private ThreadPoolExecutor parent = null; @Override public boolean offer(Runnable o) { //we can't do any checks if (parent==null) return super.offer(o); //we are maxed out on threads, simply queue the object if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o); //we have idle threads, just add it to the queue if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o); //當其線程池大小小於maximumPoolSize的時候,返回false if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false; //if we reached here, we need to add it to the queue return super.offer(o); } }
ThreadPoolExecutor的提交方法
這裏改寫了jdk線程池默認的Rejected規則,即catch住了RejectedExecutionException。正常jdk的規則是core線程數+臨時線程數 >maxSize的時候,就拋出RejectedExecutionException。這裏catch住的話,繼續往taskQueue裏頭放
package org.apache.tomcat.util.threads; import java.util.concurrent.BlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.apache.tomcat.util.res.StringManager; public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor { /** * {@inheritDoc} */ @Override public void execute(Runnable command) { execute(command,0,TimeUnit.MILLISECONDS); } public void execute(Runnable command, long timeout, TimeUnit unit) { submittedCount.incrementAndGet(); try { super.execute(command); } catch (RejectedExecutionException rx) { if (super.getQueue() instanceof TaskQueue) { final TaskQueue queue = (TaskQueue)super.getQueue(); try { if (!queue.force(command, timeout, unit)) { submittedCount.decrementAndGet(); throw new RejectedExecutionException("Queue capacity is full."); } } catch (InterruptedException x) { submittedCount.decrementAndGet(); Thread.interrupted(); throw new RejectedExecutionException(x); } } else { submittedCount.decrementAndGet(); throw rx; } } } }
重點看下queue.force 方法
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException { if ( parent.isShutdown() ) throw new RejectedExecutionException("Executor not running, can't force a command into the queue"); return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task is rejected }
注意的是這裏調用的super.offer(o,timeout,unit),即LinkedBlockingQueue,只有當列滿的時候,返回false,纔會拋出重新拋出RejectedExecutionException。
這裏改變了jdk的ThreadPoolExecutor的RejectedExecutionException拋出的邏輯,也就是超出了maxThreads不會拋出RejectedExecutionException,而是繼續往隊列丟任務,而taskQueue本身是無界的,因此可以默認幾乎不會拋出RejectedExecutionException
回顧JDK線程池策略
- 每次提交任務時,如果線程數還沒達到coreSize就創建新線程並綁定該任務。所以第coreSize次提交任務後線程總數必達到coreSize,不會重用之前的空閒線程。
- 線程數達到coreSize後,新增的任務就放到工作隊列裏,而線程池裏的線程則努力的使用take()從工作隊列里拉活來幹。
- 如果隊列是個有界隊列,又如果線程池裏的線程不能及時將任務取走,工作隊列可能會滿掉,插入任務就會失敗,此時線程池就會緊急的再創建新的臨時線程來補救。
- 臨時線程使用poll(keepAliveTime,timeUnit)來從工作隊列拉活,如果時候到了仍然兩手空空沒拉到活,表明它太閒了,就會被解僱掉。
- 如果core線程數+臨時線程數 >maxSize,則不能再創建新的臨時線程了,轉頭執行RejectExecutionHanlder。默認的AbortPolicy拋RejectedExecutionException異常,其他選擇包括靜默放棄當前任務(Discard),放棄工作隊列裏最老的任務(DisacardOldest),或由主線程來直接執行(CallerRuns).
public class ThreadPoolExecutor extends AbstractExecutorService { public void execute(Runnable command) { if (command == null) throw new NullPointerException(); /* * Proceed in 3 steps: * * 1. If fewer than corePoolSize threads are running, try to * start a new thread with the given command as its first * task. The call to addWorker atomically checks runState and * workerCount, and so prevents false alarms that would add * threads when it shouldn't, by returning false. * * 2. If a task can be successfully queued, then we still need * to double-check whether we should have added a thread * (because existing ones died since last checking) or that * the pool shut down since entry into this method. So we * recheck state and if necessary roll back the enqueuing if * stopped, or start a new thread if there are none. * * 3. If we cannot queue task, then we try to add a new * thread. If it fails, we know we are shut down or saturated * and so reject the task. */ int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); } }
總結:
tomcat的線程池與jdk的使用無界LinkedBlockingQueue主要有如下兩點區別:
- jdk的ThreadPoolExecutor的線程池增長策略是:如果隊列是個有界隊列,又如果線程池裏的線程不能及時將任務取走,工作隊列可能會滿掉,插入任務就會失敗,此時線程池就會緊急的再創建新的臨時線程來補救。而tomcat的ThreadPoolExecutor使用的taskQueue,是無界的LinkedBlockingQueue,但是通過taskQueue的offer方法覆蓋了LinkedBlockingQueue的offer方法,改寫了規則,使得它也走jdk的ThreadPoolExecutor的有界隊列的線程增長策略。
- jdk的ThreadPoolExecutor的線程池,當core線程數+臨時線程數 > maxSize,則不能再創建新的臨時線程了,轉頭執行RejectExecutionHanlder。而tomcat的ThreadPoolExecutor則改寫了這個規則,即catch住了RejectExecutionHanlder,繼續往隊列裏頭放,直到隊列滿了才拋出RejectExecutionHanlder。而默認taskQueue是無界的。