掌握使用线程池以及工程实践
各种各样的线程池
ScheduledThreadPoolExecutor 用于 延时、定时执行任务
构造方式与参数
参数
- 核心线程数,你可能想知道为什么没有最大线程数,因为他的任务队列是DelayedWorkQueue 无界的,所以达不到满队列后创建线程的条件,这只是直接原因,根本原因是这种线程并不是为了高并发而使用的,并不需要创建更多的线程来满足任务更快执行
- 线程构造工厂
- 拒绝策略,使用了无界队列了就应该知道拒绝策略不会因为队满而无法创建新线程而触发,一般情况下是线程池处于关闭状态或者异常导致拒绝策略触发
构造方式
-
public ScheduledThreadPoolExecutor(int corePoolSize)
指定线程数量,拒绝策略默认采用 AbortPolicy() 线程工厂默认采用 Executors.defaultThreadFactory()
-
public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory)
指定线程数量与线程工厂,拒绝策略默认采用 AbortPolicy()
-
public ScheduledThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler)
指定线程数量与拒绝策略,线程工厂默认采用 Executors.defaultThreadFactory()
任务队列 统一为 new DelayedWorkQueue()
常用方法
-
延时一段时间后执行一次任务
schedule(Runnable command, long delay, TimeUnit unit)
: -
带有返回值的延时执行任务
schedule(Callable<V> callable, long delay, TimeUnit unit)
: -
严格按照固定周期间隔执行任务,第一次执行需要延时initialDelay ,任务执行之间的间隔也为initialDelay
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
: -
当一个任务执行结束后再延时initialDelay 执行下一个任务,第一次执行需要延时initialDelay ,两个任务之间执行间隔为上一个任务执行时间+initialDelay
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
:
Executors 构造线程池工具类
Executors 可以快速构建一些线程池比如CachedThreadPool , SingleThreadExecutor 这两种线程池其实都是 "java 自定义出来的线程池" 来适配一些特殊场景的应用
通过Executors.newCachedThreadPool()、Executors.newSingleThreadExecutor() 以及其他静态方法...直接构造
不了解线程池构造参数的需要前往我的上一篇文章进行了解
CachedThreadPool
设想一下如果想要设计一个线程池,这个线程池需要满足能够快速响应任务执行,那么这个线程池应该是怎么样的。
- 首先要满足快速响应首先在任务队列进行下手,这个队列应该是没有容量的,也就是不需要缓冲
- 在没有了任务队列来缓冲任务执行的情况下我们还应该放开线程的创建,也就是将最大线程数量设置为接近无限也就是Integer.MAX_VALUE(虽然线程越多不一定代表处理速度越快)
那么CachedThreadPool 就是为满足任务快速执行此而生的,接下来看一下它的构造参数
java
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
- 核心线程为0,这么做的目的是让所有的线程能够回收,在空闲状态下节省系统资源
- 最大线程为Integer.MAX_VALUE 也就是优先满足任务执行
- 保活时间为60s,因为没有核心线程数量所以所有线程都可能会被回收
- SynchronousQueue 是一个0容量的队列,是对生成消费模型的一个实现,特点是放入操作会阻塞直到该放入的元素被取走,同时效率也很快。
需要注意 CachedThreadPool 是针对任务执行时间短的任务,如果执行时间较长同时并发量高可能会因为上下文切换导致效率变低,同时Integer.MAX_VALUE的最大线程数可能导致OOM!这种线程池实际在项目中使用的并不多
SingleThreadExecutor
从名字就看得出来这个是单线程的线程池,那肯定有的人要问了,我直接new Thread不行吗为什么需要构造一个只有一个线程的线程池?不要忘记线程池是有任务队列的,而SingleThreadExecutor的任务队列是LinkedBlockingQueue 也就是无界队列,当你需要一些任务能够按照入队顺序进行顺序执行的时候SingleThreadExecutor就起到作用了。
newSingleThreadScheduledExecutor
一个单线程的定时线程池,我自己使用的比较少。
在实际项目中,直接使用Executors 构造线程池的情况较少,大多数都是使用自定义参数的线程池
自定义线程池
上一篇文章已经大致了解了线程池中的参数以及线程创建和任务执行的流程,下面将介绍一些项目中常用的自定义配置
-
线程池自定义构造函数
javapublic ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
紧急线程池,用于优先满足任务执行的线程池
java
// 获取jvm可用的cpu核心数量
int availableNum = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
availableNum * 2,
availableNum * 4,
60,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
- 使用了SynchronousQueue同步队列 代表这个线程池是没有任务缓冲区,调用线程池执行任务时必须要有线程来拿取任务,否则将执行拒绝策略。
- 拒绝策略选择了CallerRunsPolicy 即调用方执行,确保了任务不会等待。
紧急线程池适用于先来的任务优先执行,不被等待的情况。但需要注意次线程池不适用于高并发同时任务时间较长的情况,因为使用了无界队列较容易触发拒绝策略导致主线程去执行任务。
后台线程池,用于不需要及时处理的后台任务
java
// 创建一个后台线程池
ExecutorService backgroundExecutor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), // 使用无界队列
new ThreadFactoryBuilder().setNameFormat("background-pool-%d").setDaemon(true).build(), // 自定义线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
- 线程数量可以设置一个较小值,因为大部分后台任务是不需要占用过多资源来处理的,任务的优先级并没有那么高
- 使用了一个无界队列,如果你担心任务数量过多导致OOM 也可以设置一个较大的值,以便触发拒绝策略让调用线程来处理掉任务,当然你也可以设置一个定时监听线程来监听后台线程池中任务队列的数量.
java
// 创建一个定时任务执行器
ScheduledExecutorService monitorExecutor = Executors.newSingleThreadScheduledExecutor();
// 定时任务:每5秒检查一次队列情况
monitorExecutor.scheduleAtFixedRate(() -> {
int queueSize = backgroundExecutor.getQueue().size();
System.out.println("Current queue size: " + queueSize);
}, 0, 5, TimeUnit.SECONDS);
- 设置为每个线程都设置为守护线程,守护线程会在jvm中没有用户线程(非守护线程)运行时自动关闭
后台线程池可以用于处理后台任务,如数据同步、定时备份等,需要注意的是任务的提交速度不应该超过线程的处理速度(定时任务基本都不是高并发的)
关键任务线程池,执行优先级高的任务
java
// 创建一个关键任务线程池
int availableNum = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor criticalTaskExecutor = new ThreadPoolExecutor(
availableNum * 2,
availableNum * 4,
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new PriorityBlockingQueue<>(), // 使用优先级队列
new ThreadFactoryBuilder().setNameFormat("critical-pool-%d").setDaemon(false).build(), // 自定义线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 线程工厂构建器
class ThreadFactoryBuilder implements ThreadFactory {
private final String nameFormat;
private final boolean isDaemon;
private int count = 0;
public ThreadFactoryBuilder setNameFormat(String nameFormat) {
this.nameFormat = nameFormat;
return this;
}
public ThreadFactoryBuilder setDaemon(boolean isDaemon) {
this.isDaemon = isDaemon;
return this;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, String.format(nameFormat, count++));
t.setDaemon(isDaemon); // 设置为非守护线程
t.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级
return t;
}
}
- 核心线程与非核心线程设置为一个较大的值以让任务快速执行
- 使用了优先级队列,以便于优先执行任务优先级别高的任务
- 使用了自定义ThreadFactoryBuilder 线程工厂,将线程的优先级设置为最高,提交任务执行效率,同时设置为非守护线程也就是用户线程,避免自动关闭
不要滥用关键任务线程池去执行非关键任务!,这会可能导致关键任务执行效率降低,同时应该有统一管理任务优先级的构造器,而不是在调用时设置一个优先级这会导致优先级混乱。
备用线程池,背后隐藏能源-启动!
java
// 创建一个主线程池
int availableNum = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor mainExecutor = new ThreadPoolExecutor(
availableNum // 核心线程数
availableNum*2, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 有界队列,容量为1000
new ThreadFactoryBuilder().setNameFormat("main-pool-%d").setDaemon(false).build(), // 自定义线程工厂
new RejectedExecutionHandler() { // 自定义拒绝策略
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("Main thread pool is busy or full, falling back to the fallback thread pool.");
fallbackExecutor.execute(r); // 使用备用线程池接管任务
}
}
);
// 创建备用线程池
ThreadPoolExecutor fallbackExecutor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new PriorityBlockingQueue<>(), // 使用优先级队列
new ThreadFactoryBuilder().setNameFormat("fallback-pool-%d").setDaemon(false).build(), // 自定义线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
- 使用备用线程池需要修改主线程池的拒绝策略,将拒绝的任务交给备用线程池执行
写在最后
任务一般分为IO密集型任务与cpu密集型任务,如果是cpu密集型任务,那么建议核心线程数量就设置为cpu数量-1,最大线程数就为核心线程数量,因为jvm中的线程与操作系统的线程是1:1对应的,实际调度还得靠操作系统,过多的线程并不会增加处理效率反而可能会因为操作系统线程上下文切换导致性能损耗。至于IO密集型任务最大线程数量可以设置为2*cpu数量,或者使用java协程(不需要池化管理)