Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService
。线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
ThreadPoolExecutor
执行execute方法分下面4种情况。
corePoolSize
,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)corePoolSize
,则将任务加入BlockingQueue
。BlockingQueue
(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。maximumPoolSize
,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()
方法。分析任务的特性
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()
方法获得当前设备的CPU个数。
如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。
建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是出现这样问题时也会影响到其他任务。
确定线程数
如果所有的任务都是计算密集型的,则创建处理器可用核心数个线程就可以。
如果一个任务执行IO操作,其线程将被阻塞,于是处理器可以立即进行上下文切换以便处理其他就绪线程
如果任务有50%的时间处于阻塞状态,则程序所需线程数为处理器可用核心数的两倍
如果任务被阻塞的时间少于50%,即这些任务是计算密集型的,则程序所需线程数将随之减少,但最少也不应低于处理器的核心数。如果任务被阻塞的时间大于执行时间,即该任务是IO密集型,此时就需要创建比处理器核心数大几倍的线程
线程数=CPU可用核心数/(1-阻塞系数)
,其中阻塞系数的取值在0和1之间。计算密集型任务的阻塞系数为0,IO密集型任务的阻塞系数则接近于1
由于web服务器的请求大部分时间都花在等待服务器响应上,所以阻塞系数相当高,因此程序需要开的线程数可能是处理器核心数的若干倍。假设阻塞系数是0.9,即每个任务90%的时间处于阻塞状态,而只有10%的时间在处理响应。则在双核处理器上我们就需要开启20个线程。
taskCount
:线程池需要执行的任务数量。completedTaskCount
:线程池在运行过程中已完成的任务数量,小于或等于taskCount
。largestPoolSize
:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。getPoolSize
:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。getActiveCount
:获取活动的线程数。beforeExecute
、afterExecute
和terminated
方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。重要的类
ExecutorService | 线程池接口 |
---|---|
ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题 |
ThreadPoolExecutor | ExecutorService的默认实现 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
在Executors类里面提供了一些静态工厂,生成一些常用的线程池
newSingleThreadExecutor
:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。newFixedThreadPool
:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变(剩下的任务将被阻塞直到执行器有空闲的线程可用),如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。newCachedThreadPool
:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。仅当线程的数量是合理的或者线程只会运行很短的时间时,适合创建可缓存的线程池。newScheduledThreadPool
:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求,开发定时任务程序时推荐使用此线程池每个ExecutorService
代表一个线程池,其作用是将线程的创建与执行过程分离开,而不是将线程的生命周期和任务的执行过程绑定在一起。如果不需要线程池,我们可以调用shutdown()来关掉线程池,该方法不会立即将线程销毁,而是会等所有当前已经被调度的任务进行结束之后才会将其关闭,并且在调用该方法之后我们就不能调用任何新任务了。
ThreadPoolExecutor通过submit()发送一个callable对象给执行器去执行,这个submit方法接收callable作为参数,并返回Future对象。Future对象可以用于以下两个目的:
调用Future对象的get()方法时候,如果Future对象所控制的任务并未完成,那么这个方法将一直阻塞到任务完成。如果想取消一个已经发送给任务执行器的任务,可以使用Future接口的cancel()方法
线程池的关键点是:
任务耗时
对于任务耗时短的情况,要求线程尽量少,如果线程太多,有可能出现线程切换和管理的时间,大于任务执行的时间,那效率就低了;
对于耗时长的任务,要分是cpu任务,还是io等类型的任务。如果是cpu类型的任务,线程数不宜太多;但是如果是io类型的任务,线程多一些更好,可以更充分利用cpu。
并发度
上下文切换
当我们在调试程序将JVM中线程导出Dump时,会出现pool-N-thread-M
这样的提示,这是缺省的线程池名称,其中N代表池的序列号,每次你创建一个新的线程池,这个N数字就增加1;而M是这个线程池中的线程顺序。举例, pool-2-thread-3
表示在第二个线程池中的第三个线程,JDK将这种命名策略封装在ThreadFactory
,Guava可以帮助你方便命名:
final ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("monitor-%d")
.setDaemon(true)
.build();
final ExecutorService executorService = Executors.newFixedThreadPool(10, threadFactory);
一旦我们记住线程的名称,我们就可以在运行时改变它们,这是有意义的,因为线程dump只显示类和方法名称,没有参数和本地变量。通过调整线程名称可以保留一些比较关键的上下文信息,以便我们方便跟踪具体的消息、记录、查询或者找出引起死锁的原因,如下:
private void process(String messageId) {
executorService.submit(() -> {
final Thread currentThread = Thread.currentThread();
final String oldName = currentThread.getName();
currentThread.setName("Processing-" + messageId);
try {
//real logic here...
} finally {
currentThread.setName(oldName);
}
});
}
在try-finally中线程被取名为Processing-消息ID,这样我们就能跟踪是哪个消息流经系统。
显式地安全地关闭线程: 当程序要关闭时,需要注意任务队列中任务的情况如何以及正在运行的任务执行的情况如何。如果要舍弃任务则执行shutdownNow()
,如果要让所有任务都执行完毕,则执行shutdown()
private void sendAllEmails(List<String> emails) throws InterruptedException {
emails.forEach(email ->
executorService.submit(() ->
sendEmail(email)));
executorService.shutdown();
//等待一分钟直到任务执行完成,1min后还是有任务没执行到,则返回false,剩下的任务依然会执行完
final boolean done = executorService.awaitTermination(1, TimeUnit.MINUTES);
log.debug("{}", done);
}
InterruptedException
的方法时,你可以在这个线程中调用Thread.interrupt()
,大多数堵塞的方法将立即抛出InterruptedException
.如果你将任务提交给线程池(ExecutorService.submit()),当任务已经在执行时,你能调用Future.cancel(true)
,在这种情况下线程池将试图中断线程运行你的任务,你能很有效率的中断你的任务。不正确大小的线程池可能会导致缓慢、不稳定和内存泄漏。 如果您配置线程太少,将建立队列消耗大量的内存。 另一方面,太多的线程由于过度上下文切换会减慢整个系统,导致相同的症状。 查看队列深度很重要,保持它有界,以便超负荷的线程池可以暂时拒绝新任务
final BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
executorService = new ThreadPoolExecutor(n, n,
0L, TimeUnit.MILLISECONDS,
queue);
一个固定100大小的ArrayBlockingQueue。也就是说如果已经有100个任务在队列中了(还有N个在执行中),新的任务就会被拒绝掉,并抛出RejectedExecutionException
异常。由于这里的队列是在外部声明的,我们还可以时不时地调用下它的size()方法来将队列大小记录在到日志/JMX/或者你所使用的监控系统中。
以下代码的输出结果是什么?
executorService.submit(() -> {
System.out.println(1 / 0);
});
它不会打印任何东西。也没有抛出java.lang.ArithmeticException: / by zero
的错误,线程池自己吞进了这个Exception
,好像从来没有发生一样,这是线程池与普通线程的区别,如果你自己创建一个Thread并提交一个Runnable,你必须自己使用try-catch环抱方法体(或者使用UncaughtExceptionHandler),至少记录日志,如果你提交Callable确保你总是能解除引用,使用堵塞get()到re-throw exception:
final Future<Integer> division = executorService.submit(() -> 1 / 0);
//下面将抛出由除零ArithmeticException引起的ExecutionException
division.get();
Spring框架的@Async曾经也有这个bug
@Test
public void threadException(){
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2);
Future<String> future = newFixedThreadPool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println(1/0);
System.out.println("执行........");
return "打印结果";
}
});
/***
* 如果仅仅只是执行上面,不会抛异常
*/
/*try {
String print = future.get();
logger.debug(print);
} catch (InterruptedException | ExecutionException e) {
logger.error(e.getMessage(),e);
e.printStackTrace();
}finally{
newFixedThreadPool.shutdown();
}*/
}
监控工作队列的长度只是一个方面。然而排除故障时查看从提交任务到实际执行之间的时间差就显得非常重要了。这个时间差越接近0就越好(说明正好线程池中有空闲的线程),否则任务要入队的话这个时间就会增加了。再进一步说,如果线程池不是固定线程数的话,执行新的任务还得新创建一个线程,这个同样也会消耗一定的时间。为了能更好地监控这项指标,可以包装ExecutorService(装饰模式)
public class WaitTimeMonitoringExecutorService implements ExecutorService {
private final ExecutorService target;
public WaitTimeMonitoringExecutorService(ExecutorService target) {
this.target = target;
}
@Override
public <T> Future<T> submit(Callable<T> task) {
final long startTime = System.currentTimeMillis();
return target.submit(() -> {
final long queueDuration = System.currentTimeMillis() - startTime;
log.debug("Task {} spent {}ms in queue", task, queueDuration);
return task.call();
}
);
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
return submit(() -> {
task.run();
return result;
});
}
@Override
public Future<?> submit(Runnable task) {
return submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
task.run();
return null;
}
});
}
//...
}
当我们将任务提交给线程池的时候,便立即开始记录它的时间。一旦这个任务被取出并开始执行时便停止计时。不要被代码中的startTime和queueDuration这两个变量搞混了。事实上它们是在两个不同的线程中进行求值的,通常都会差个毫秒级或者秒级
在Java 8引入了更强大CompletableFuture
。 请尽可能使用它。ExecutorService
并没有扩展以支持这个增强型的接口,因此你得自己动手了。这么写是不行的了
final CompletableFuture<BigDecimal> future =
CompletableFuture.supplyAsync(this::calculate, executorService);
SynchronousQueue
是一个非常有意思的BlockingQueue
。它本身甚至都算不上是一个数据结构。最好的解释就是它是一个容量为0的队列。这里引用下Java文档中的一段话:每一个insert操作都需要等待另一个线程的一个对应的remove操作,反之亦然。同步队列内部不会有任何空间,甚至连一个位置也没有。你无法对同步队列执行peek操作,因为仅当你要移除一个元素的时候才存在这么个元素;如果没有别的线程在尝试移除一个元素你也无法往里面插入元素;你也无法对它进行遍历,因为它什么都没有。同步队列与CSP和Ada中所用到的集结管道(rendezvous channel)有异曲同工之妙
在要么执行要么马上丢弃后台执行的场景下同步队列非常有用。我们创建了一个拥有两个线程的线程池,以及一个SynchronousQueue
。由于SynchronousQueue
本质上是一个容量为0的队列,因此这个ExecutorService只有当有空闲线程的时候才能接受新的任务。如果所有的线程都在忙,新的任务便会马上被拒绝掉,不会进行等待
BlockingQueue<Runnable> queue = new SynchronousQueue<>();
ExecutorService executorService = new ThreadPoolExecutor(n, n,
0L, TimeUnit.MILLISECONDS,
queue);