引言
《Java 开发手册》 编程规约|并发处理中指出
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
规约中提到java.util.concurrent.ThreadPoolExecutor
,我们来看下它的构造方法
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明
参数名 | 说明 |
---|---|
corePoolSize | 线程池维护线程的最少数量。线程池至少会保持改数量的线程存在,即使没有任务可以处理。(注意:这里说的至少是指线程达到这个数量后,即使有空闲的线程也不会释放,而不是说线程池创建好之后就会初始化这么多线程) |
maximumPoolSize | 池中允许的最大线程数 |
keepAliveTime | 线程池维护线程所允许的空闲时间。当线程池中的线程数量大于 corePoolSize时,超过corePoolSize的线程如果空闲时间超过keepAliveTime,线程将被终止 |
unit | keepAliveTime参数的时间单位 |
workQueue | 在执行任务之前用于保留任务的队列。 此队列将仅保存execute方法提交的Runnable任务。 |
threadFactory | 执行程序创建新线程时要使用的工厂,java.util.concurrent.Executors.DefaultThreadFactory |
handler | 线程池对拒绝任务的处理策略。因达到线程界限和队列容量而被阻止执行时使用的处理程序.AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy、自定义 |
BlockingQueue类型
参数名 | 说明 |
---|---|
LinkedBlockingQueue | 无界队列,FIFO(先进先出),可以无限向队列中添加任务,直到内存溢出,newSingleThreadExecutor、newFixedThreadPool |
ArrayBlockingQueue | 有界队列,FIFO,需要指定队列大小,如果队列满了,会触发线程池的RejectedExecutionHandler逻辑 |
SynchronousQueue | 一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。可以简单理解为是一个容量只有1的队列。Executors.newCachedThreadPool使用的是这个队列newCachedThreadPool |
PriorityBlockingQueue | 优先级队列,线程池会优先选取优先级高的任务执行,队列中的元素必须实现Comparable接口 |
DelayedWorkQueue | 专门的延迟队列。 为了与TPE声明相啮合,必须将此类声明为BlockingQueue 即使它只能容纳RunnableScheduledFutures。newScheduledThreadPool |
RejectedExecutionHandler类型
参数名 | 说明 |
---|---|
AbortPolicy | 中止政策。线程池默认的策略,如果元素添加到线程池失败,会抛出RejectedExecutionException异常 |
DiscardPolicy | 丢弃政策。如果添加失败,则放弃,并且不会抛出任何异常(空实现) |
DiscardOldestPolicy | 放弃最早的政策。如果添加到线程池失败,会将队列中最早添加的元素移除,再尝试添加,如果失败则按该策略不断重试 |
CallerRunsPolicy | 除非执行器已关闭,否则在调用者线程中执行任务r(使用run方法),在这种情况下,该任务将被丢弃。 |
自定义 | 如果觉得以上几种策略都不合适,那么可以自定义符合场景的拒绝策略。需要实现RejectedExecutionHandler接口,并将自己的逻辑写在rejectedExecution方法内。 |
原理
有请求时,创建线程执行任务,当线程数量等于corePoolSize时,请求加入阻塞队列里,当队列满了时,接着创建线程,线程数等于maximumPoolSize。 当任务处理不过来的时候,线程池开始执行拒绝策略。
Java中Executors提供了4种线程池
newSingleThreadExecutor
创建一个单线程的线程池,该执行程序使用单个工作线程在不受限制的队列中操作。 (但是请注意,如果单个线程由于在关闭之前执行期间由于执行失败而终止,则新线程将在需要执行后续任务时取而代之。)保证任务按顺序执行,且最多执行一个任务将在任何给定时间处于活动状态
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newFixedThreadPool
创建一个固定数量的线程池,该线程池可重用固定数量的线程在共享的无界队列上操作。在任何时候,最多 {@code nThreads}个线程将是活动的处理任务。 如果在所有线程都处于活动状态时提交了其他任务,则它们将在队列中等待,直到某个线程可用为止。 如果任何线程由于执行过程中的失败而终止在关闭之前如果需要执行一个新任务,则将替换一个新线程。直到显式{@link ExecutorService#shutdown shutdown}之前,池中的线程将一直存在。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool
创建一个线程池,该线程池根据需要创建新线程,但是会在可用时重用以前构造的线程。这些池通常将提高执行许多短期异步任务的程序的性能。 调用{@code execute}将重用以前构造的线程(如果有)。如果没有可用的现有线程,则将创建一个新的线程并将其添加到池中。 六十秒未使用的线程将终止并从缓存中删除。因此,保持空闲时间足够长的池将不会消耗任何资源。请注意,可以使用{@link ThreadPoolExecutor}构造函数创建具有类似属性但不同详细信息(例如,超时参数)的池。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newScheduledThreadPool
创建一个线程池,该线程池可以计划命令在给定的延迟后运行或定期执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
自定义ThreadFactory
- 规约中已提供
/**
* 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
*
* 正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给 whatFeaturOfGroup
*
* @author ylm-sigmund
* @since 2020/12/15 19:42
*/
public class UserThreadFactory implements ThreadFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(UserThreadFactory.class);
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
/**
* 定义线程组名称,在 jstack 问题排查时,非常有帮助
*
* @param whatFeatureOfGroup
* whatFeatureOfGroup
*/
public UserThreadFactory(String whatFeatureOfGroup) {
namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-Worker-";
}
/**
* 创建线程
*
* @param task
* Runnable
* @return Thread
*/
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(task, name);
LOGGER.info("UserThreadFactory newThread'name={}", thread.getName());
return thread;
}
}
自定义RejectedExecutionHandler
RejectedExecutionHandler customRejectedExecutionHandler = (Runnable runnable, ThreadPoolExecutor executor) -> {
LOGGER.error(
"customRejectedExecutionHandler:The thread pool is full and the task is discarded,ThreadPoolExecutor={}",
executor.toString());
};
合理配置线程池大小
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
任务的优先级:高,中和低。
任务的执行时间:长,中和短。
任务的依赖性:是否依赖其他系统资源,如数据库连接。
根据任务所需要的cpu和io资源的量可以分为,
CPU密集型任务: 主要是执行计算任务,响应时间很快,cpu一直在运行,这种任务cpu的利用率很高。
IO密集型任务:主要是进行IO操作,执行IO操作的时间较长,这是cpu出于空闲状态,导致cpu的利用率不高。
为了合理最大限度的使用系统资源同时也要保证的程序的高性能,可以给CPU密集型任务和IO密集型任务配置一些线程数。
CPU密集型:线程个数为CPU核数。这几个线程可以并行执行,不存在线程切换到开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗
IO密集型:线程个数为CPU核数的两倍。到其中的线程在IO操作的时候,其他线程可以继续用cpu,提高了cpu的利用率。
返回可用于Java虚拟机的处理器数量。 在虚拟机的特定调用期间,此值可能会更改。 因此,对可用处理器数量敏感的应用程序应该偶尔轮询此属性并适当地调整其资源使用情况。
返回值: CPU核数,虚拟机可用的最大处理器数量; 永远不小于一个
Runtime.getRuntime().availableProcessors();