每次新开线程去执行任务,运行完任务销毁线程,都会消耗资源(操作系统级别的线程)
线程池就可以提前创建线程、保留线程,节约资源、节省掉开辟线程和销毁线程的消耗、提高效率
CPU密集型任务:依靠CPU去进行计算。线程池开CPU核心数+1个线程(任务线程中断/阻塞的情况,保证CPU仍然有任务执行,充分压榨CPU的性能所以+1)
IO密集型任务:网络相关,文件IO,网络IO,磁盘IO。例:等待MySQL返回结果,等待接口返回响应;等待阻塞时不需要用到CPU。理论上执行IO的时间越长,可以开辟更多的线程。线程数和IO时间有关,和CPU核数无关
混合任务:根据业务场景拆分
理论上:线程数 = CPU核心数 * (1 + 线程等待时间/线程运行总时间)
通常先计算出理论的线程数,再去压测尽量模拟生产环境,得到最优的线程数。
队列大小:队列必须是阻塞队列,能容忍最多几秒拿到结果,能接受的最多的排队时间,根据任务平均执行时间计算。(假设通过压测得到最优线程数为100,平均任务执行时间1s,也就是说1s可以处理完100个任务。这种情况下,队列长度如果设置500,就代表从进入队列到拿到结果最多要经历5s的时间,等待5s可以接受就能设置队列长度为500,接受不了就必须砍掉队列长度,根据实际容忍排队时间来设置队列长度)
核心线程数:假设通过压测得到最优线程数为500,核心业务/请求数多的可以设置为500;其他业务/几分钟请求一次这种,可以设置20或者其他,设置小一些没关系
最大线程数:假设通过压测得到最优线程数为500,可以设置最大线程数为500,也可以设置其他压测结果ok的。
最大空闲时间:1分钟,半分钟都可以
1、void execute(Runnable command)
package java.util.concurrent;
// ThreadPoolExecutor#execute
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);
}
2、Future submit(Runnable task)
submit中最终还是调用的execute()方法,区别是会返回一个Future对象,用来获取任务执行结果
package java.util.concurrent;
// ThreadPoolExecutor extends AbstractExecutorService
// AbstractExecutorService#submit
public Future> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
execute(Runnable command)方法执行步骤:
注意:提交一个Runnable时,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建新线程。
注意:ThreadPoolExecutor相当于是非公平的,比如队列满了之后提交的Runnable可能会比正在排队的Runnable先执行。
非公平体现在:线程数大于核心线程数,小于最大线程数,阻塞队列满的情况下,新添加的任务可能先于队列中的任务执行
1、Thread类提供了一个stop(),但是标记了@Deprecated,为什么不推荐用stop()方法来停掉线程呢?
因为stop()方法太粗暴了,一旦调用了stop(),就会直接停掉线程,但是调用的时候根本不知道线程刚刚在做什么,任务做到哪一步了,这是很危险的。
注意:stop()会释放线程占用的synchronized锁(不会自动释放ReentrantLock锁,这也是不建议用stop()的一个因素)。
2、非核心线程,超过最大空闲时间后,自动销毁关闭
(注意:核心线程和非核心线程实际上没有区别,哪个线程CAS获取到锁,就会销毁哪个线程)
3、线程的内部发生异常时,线程关闭销毁。(销毁后小于核心线程数时,立刻补充创建一个空任务的线程)
4、推荐:自定义一个变量(默认false,修改为true 后结束线程),或者通过中断 interrupt 停掉线程,线程池就是用的 interrupt。自定义变量的好处是:线程自身可以控制到底要不要停止,何时停止
注意:线程sleep过程中如果被中断了会接收到异常。
// ThreadPoolExecutor#shutdownNow
public List shutdownNow() {
List tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
// ThreadPoolExecutor#interruptWorkers
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
// ThreadPoolExecutor#Worker#interruptIfStarted
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
线程池中的线程在运行过程中,执行完创建线程时绑定的第一个任务后,就会不断的从队列中获取任务并执行,那么如果队列中没有任务了,线程为了不自然消亡,就会阻塞在获取队列任务时,等着队列中有任务过来就会拿到任务从而去执行任务。
通过这种方法能最终确保,线程池中能保留指定个数的核心线程数
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
线程阻塞在这里,核心线程可以保活;并且阻塞队列中有任务时,可以立刻取出来执行
// ThreadPoolExecutor#getTask
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
某个线程在从队列获取任务时,会判断是否使用超时阻塞获取,我们可以认为非核心线程会poll(),核 心线程会take(),非核心线程超过时间还没获取到任务后面就会自然消亡了。
会的,那有没有可能核心线程数在执行任务时都出错了,导致所有核心线程都被移出了线程池?
在源码中,当执行任务时出现异常时,最终会执行processWorkerExit(),执行完这个方法后,当前线程也就自然消亡了,但是!processWorkerExit()方法中会额外再新增一个线程,这样就能维持住固定的核心线程数。
Tomcat中用的线程池为org.apache.tomcat.util.threads.ThreadPoolExecutor,注意类名和JUC下的 一样,但是包名不一样。
Tomcat创建这个线程池:
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);
}
TaskQueue的入队逻辑:
public boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) {
return super.offer(o);
}
// 如果线程池的线程个数等于最大线程池数才入队。注意和JUC下面线程池入队的区别
//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);
}
// 线程池的线程个数小于最大线程池数,返回false,入队失败。这里表示创建线程直到达到最大线程数
//if we have less threads than maximum force creation of a new thread
if (parent.getPoolSize()
总结Tomcat的线程池:随着任务的提交,会优先创建线程,直到线程个数等于最大线程数才会入队。
创建Tomcat的ThreadPoolExecutor时,自动创建核心线程。JUC的ThreadPoolExecutor不会提前创建核心线程
当线程数小于最大线程数,并且存在空闲线程时,优先入队使用空闲线程来执行任务,充分利用线程资源
思考:线程池的五种状态是如何流转的?
1、CPU密集型任务:CPU核心数+1,这样既能充分利用CPU,也不至于有太多的上下文切换成本
2、IO型任务:建议压测,或者先用公式计算出一个理论值(理论值通常都比较小)
3、对于核心业务(访问频率高),可以把核心线程数设置为我们压测出来的结果,最大线程数可以等于核心线程数,或者大一点点,比如我们压测时可能会发现500个线程最佳,但是600个线程时也还行,此时600就可以为最大线程数
4、对于非核心业务(访问频率不高),核心线程数可以比较小,避免操作系统去维护不必要的线程,最大线程数可以设置为我们计算或压测出来的结果。