线程池
-
说一说几种常见的线程池及适用场景?
线程池创建可以有2种方式,Executors或用ThreadPoolExecutor自己手动创建,其实Executos也是用ThreadPoolExecutor创建的,只不过它是自己封装了一些入参。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, //默认线程工程,新建线程 RejectedExecutionHandler handler) //默认是终止的拒绝策略
newCacheThreadPool:可缓存的线程池
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
newScheduledThreadPool:定期执行任务的线程池
new ScheduledThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
newFixedThreadPool:固定大小的线程池
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
newSingleThreadExecutor:单线程的线程池,可用于顺序执行
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
阿里巴巴java开发规范中指出,创建线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor创建,这样可以让开发同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors创建的线程池弊端如下:
1)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量是Integer.MAX_VALUE,可能会穿就创建大量线程,导致OOM。
2)FixedPoolExecutor和SingleThreadExecutor:允许的请求队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM。FixedPool的核心线程数比较小时,和SingleThread的核心线程数是1,不创建大量的线程,那是怎么可以堆积请求的呢?这就和工作队列有关系了。
-
定时任务scheduleThreadPoolExecutor
测试代码
public static void testScheduledThreadPool() {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
// ses.scheduleAtFixedRate(new Task("myScheduledTask"),4,3,TimeUnit.SECONDS);
ses.scheduleWithFixedDelay(new Task("myScheduledTask"),4,3,TimeUnit.SECONDS);
System.out.println("have scheduled the task " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
//ses.shutdown();
}
static class Task implements Runnable {
private final String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("\nstart task " + name + " " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end task " + name + " " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
scheduleAtFixedRate,开始任务开始算固定间隔,如果任务执行时间超过了间隔时间就马上执行。
have scheduled the task 2021-10-12 11:18:38
start task myScheduledTask 2021-10-12 11:18:42
end task myScheduledTask 2021-10-12 11:18:43
start task myScheduledTask 2021-10-12 11:18:45
end task myScheduledTask 2021-10-12 11:18:46
start task myScheduledTask 2021-10-12 11:18:48
end task myScheduledTask 2021-10-12 11:18:49
scheduleWithFixedDelay,结束任务开始算固定间隔,如果任务执行时间超过了间隔时间依旧延后间隔时间执行。
have scheduled the task 2021-10-12 11:21:54
start task myScheduledTask 2021-10-12 11:21:58
end task myScheduledTask 2021-10-12 11:21:59
start task myScheduledTask 2021-10-12 11:22:02
end task myScheduledTask 2021-10-12 11:22:03
start task myScheduledTask 2021-10-12 11:22:06
end task myScheduledTask 2021-10-12 11:22:07
start task myScheduledTask 2021-10-12 11:22:10
end task myScheduledTask 2021-10-12 11:22:11
为了测试任务时长超过间隔时间,可以把sleep改成5秒。
扩展:spring的定时任务实现原理?
-
线程池都有哪几种工作队列?
1)直接提交SynchronousQueue,size写死0,isEmpty()一直true
2)有界限队列ArrayBlockingQueue,数组实现,容量固定
3-1)无界队列也可以无界LinkedBlockingQueue,FIFO,也可以是有界限的,当构造函数传了容量大小就是有界队列【另外有个LinkedBlockingDeque双向队列线程池用不上】,如果是无界队列,maxPoolSize就没有作用了。
3-2)无界队列PriorityBlockingQueue,Comparator规则,数组实现,初始化容量大小11,可扩容
-
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
1)高并发、任务执行时间短: corePoolSize=CPU数量+1,减少线程上下文的切换,maxSize=尽可能大
2)并发不高、任务执行时间长:a)如果执行时间长在IO操作上,corePoolSizek可设置大一些,让CPU处理更多的业务 ; b)如果执行时间长在计算上,没有办法了,和1)一样,尽量减少线程上下文切换。
3)高并发、任务执长:解决这类问题关键不在怎么设置线程池的大小,而在于整体架构的设计,看看业务上是否运行做缓存,然后考虑增加服务器,最后看看能否对任务进行拆分,分解步骤,看看是否有些步骤可以放在其他地方执行。corePoolSize也是=CPU数量+1。
4)并发不高、任务执行时间短:用不用线程关系不大。
-
线程池的拒绝策略
JDK提供了4个,默认是终止策略
AbortPolicy终止策略,抛出异常,终止流程。
DiscardPolicy静默丢弃,不抛异常
DiscardOldestPolicy静默丢弃最早的任务,执行最新的任务,不抛异常;适用于老的任务是可以丢弃的情况,旧的任务没处理完,新的任务又来了,旧的任务丢弃不会造成影响。
CallerRunsPolicy调用者运行策略,适用于不允许失败,对性能要求不高,并发量较小的情况,当多次提交任务时,阻塞后续任务执行,性能和效率会下降。
三方策略
1)打印线程池当前状态日志、打印堆栈
2)新起线程,可能会耗尽系统资源
3)责任链模式,可以依次执行多个拒绝策略。
问:线程执行过程中抛异常了,后续的线程是否还能继续执行?
答:如果是带返回值的,后续线程会被中断;如果是不带返回值的,后续线程不会被中断。
问:使用的是AbortPolicy,触发拒绝策略时,后续的线程还会继续执行吗?
答:不会。
问:什么情况下才会触发拒绝策略呢?
答:
当提交的线程数量超过核心线程数,并且超过阻塞队列的大小,且超过最大线程数量就会触发拒绝策略。
-
新建线程的过程是怎么样的?
7.线程池监控
/**
* 该类继承ThreadPoolExecutor类,覆盖了shutdown(), shutdownNow(), beforeExecute() 和 afterExecute()
* 方法来统计线程池的执行情况
*
*/
public class ExecutorsUtil extends ThreadPoolExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorsUtil.class);
// 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间
private ConcurrentHashMap<String, Date> startTimes;
// 线程池名称,一般以业务名称命名,方便区分
private String poolName;
/**
* 调用父类的构造方法,并初始化HashMap和线程池名称
*
* @param corePoolSize
* 线程池核心线程数
* @param maximumPoolSize
* 线程池最大线程数
* @param keepAliveTime
* 线程的最大空闲时间
* @param unit
* 空闲时间的单位
* @param workQueue
* 保存被提交任务的队列
* @param poolName
* 线程池名称
*/
public ExecutorsUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
String poolName) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new EventThreadFactory(poolName));
this.startTimes = new ConcurrentHashMap<>();
this.poolName = poolName;
}
/**
* 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计线程池情况
*/
@Override
public void shutdown() {
// 统计已执行任务、正在执行任务、未执行任务数量
LOGGER.info(String.format(this.poolName + " Going to shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
super.shutdown();
}
/**
* 线程池立即关闭时,统计线程池情况
*/
@Override
public List<Runnable> shutdownNow() {
// 统计已执行任务、正在执行任务、未执行任务数量
LOGGER.info(
String.format(this.poolName + " Going to immediately shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
return super.shutdownNow();
}
/**
* 任务执行之前,记录任务开始时间
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTimes.put(String.valueOf(r.hashCode()), new Date());
}
/**
* 任务执行之后,计算任务结束时间
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
Date finishDate = new Date();
long diff = finishDate.getTime() - startDate.getTime();
// 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止
LOGGER.info(String.format(this.poolName
+ "-pool-monitor: Duration: %d ms, PoolSize: %d, CorePoolSize: %d, Active: %d, Completed: %d, Task: %d, Queue: %d, LargestPoolSize: %d, MaximumPoolSize: %d, KeepAliveTime: %d, isShutdown: %s, isTerminated: %s",
diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(), this.getCompletedTaskCount(), this.getTaskCount(),
this.getQueue().size(), this.getLargestPoolSize(), this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS),
this.isShutdown(), this.isTerminated()));
}
}
8.线程池是怎么复用线程的?又是怎么回收线程的?
复用:参考
通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中不停地取任务,并直接调用 Runnable 的 run 方法来执行任务,这样就保证了每个线程都始终在一个循环中,反复获取任务,然后执行任务。
回收:参考
在getTask()返回null时,会从workQuery remove线程进行回收,getTask返回空的情况有2种情况:
工作线程数大于核心线程数、从workQuery阻塞队列中获取超时了,这个超时时间就是构造函数入参的keepAliveTime,当变量allowCoreTimeOut设置true时核心线程也会被回收。
参考:实现原理及美团业务实战