目录
- 1.ThreadPoolExecutor 介绍
- 2.ThreadPoolExecutor 参数
- 3.拒绝策略
- 4.ThreadPoolExecutor中get方法
- 5.线程池的执行顺序
- 6.线程池的四种状态
- 7. 如何设置线程池参数
- 8. submit 和 execute 区别
- 9. 线程池使用规范
使用线程池可以避免频繁地创建和销毁线程会带来显著的性能开销,线程池的管理通常依赖于特定的编程框架或库,其中ThreadPoolExecutor
是Java中一个非常常用的线程池管理工具。以下是关于如何管理线程池的一些关键点,结合了参考文章中的相关信息:
1.ThreadPoolExecutor 介绍
定义:ThreadPoolExecutor是Java中用于管理线程池的一个类,它提供了任务管理、线程的调度和相关的钩子方法来控制线程池的状态。
方法:
-
execute()
:用于提交任务到线程池执行,忽略任务执行结果。 -
submit()
:用于提交任务到线程池执行,并可以获取任务执行结果。 -
shutdown()
:关闭线程池,但会等待任务队列中的任务都执行完毕后再关闭。 -
shutdownNow()
:直接关闭线程池,并将任务队列中的任务导出到一个列表中返回。
创建线程池的类是ThreadPoolExecutor
,继承关系如下:
其中 ThreadPoolExecutor
提供了4个有参构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
2.ThreadPoolExecutor 参数
- corePoolSize(必需):核心线程数。即池中一直保持存活的线程数,即使这些线程处于空闲。但是将
allowCoreThreadTimeOut
参数设置为true后,核心线程处于空闲一段时间以上,也会被回收。 - maximumPoolSize(必需):池中允许的最大线程数。当核心线程全部繁忙且任务队列打满之后,线程池会临时追加线程,直到总线程数达到
maximumPoolSize
这个上限。 - keepAliveTime(必需):线程空闲超时时间。当非核心线程处于空闲状态的时间超过这个时间后,该线程将被回收。将
allowCoreThreadTimeOut
参数设置为true后,核心线程也会被回收。 - unit(必需):
keepAliveTime
参数的时间单位。有:TimeUnit.DAYS(天)、TimeUnit.HOURS(小时)、TimeUnit.MINUTES(分钟)、TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)、TimeUnit.MICROSECONDS(微秒)、TimeUnit.NANOSECONDS(纳秒) - workQueue(必需):任务队列,采用阻塞队列实现。当核心线程全部繁忙时,后续由
execute
方法提交的Runnable将存放在任务队列中,等待被线程处理。 - threadFactory(可选):线程工厂。指定线程池创建线程的方式。
- handler(可选):拒绝策略。当线程池中线程数达到
maximumPoolSize
且workQueue
打满时,后续提交的任务将被拒绝,handler可以指定用什么方式拒绝任务。
3.拒绝策略
拒绝策略需要实现RejectedExecutionHandler接口,不过Executors框架已经为我们实现了4种拒绝策略:
- AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。(下面会举例)
- CallerRunsPolicy:直接运行这个任务的run方法,但并非是由线程池的线程处理,而是交由任务的调用线程处理。
- DiscardPolicy:直接丢弃任务,不抛出任何异常。
- DiscardOldestPolicy:将当前处于等待队列列头的等待任务强行取出,然后再试图将当前被拒绝的任务提交到线程池执行。
如何正常选用拒绝策略
- 如果任务允许失败,可以选择 DiscardPolicy 拒绝策略
- 如果不允许失败,则可以选择CallerRunsPolicy 拒绝策略,并适当调高等待队列。
- 如果并发量达到单机瓶颈,或是要求更高反应效率,就可以考虑使用MQ了
线程初始化:
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
prestartCoreThread():boolean prestartCoreThread(),初始化一个核心线程
prestartAllCoreThreads():int prestartAllCoreThreads(),初始化所有核心线程,并返回初
始化的线程数
public boolean prestartCoreThread() {
return addIfUnderCorePoolSize(null); //注意传进去的参数是null
}
public int prestartAllCoreThreads() {
int n = 0;
while (addIfUnderCorePoolSize(null))//注意传进去的参数是null
++n;
return n;
}
线程池容量调整:
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:
- setCorePoolSize:设置核心池大小
- setMaximumPoolSize:设置线程池最大能创建的线程数目大小
4.ThreadPoolExecutor中get方法
- getActiveCount():获取线程池正在执行任务的线程数量
- getCorePoolSize():获取线程池核心线程的大小 = corePoolSize
- getMaximumPoolSize():获取线程池最大线程数 = maximumPoolSize
- getPoolSize(): 获取线程池中存活的线程数量(无论工作还是空闲)
- getQueue().size():获取队列中排队的线程数量(进来的线程)
- getCompletedTaskCount():获取执行完成的线程数(进来的线程)
- getLargestPoolSize():线程池中最大同时执行的线程数
5.线程池的执行顺序
创建一个
- 核心线程为:2,
- 最大线程为:3,
- 非核心线程空闲超过200毫秒回收,
- 队列长度为:2
的线程池。
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(2));
for(int i=0;i<6;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
executor.getQueue().size()+",已执行完的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
static class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task "+taskNum);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"执行完毕");
}
}
打印:
线程池中线程数目:1,队列中等待执行的任务数目:0,已执行完的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:1,已执行完的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:2,已执行完的任务数目:0
线程池中线程数目:3,队列中等待执行的任务数目:2,已执行完的任务数目:0
正在执行task 0
正在执行task 4
正在执行task 1
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.dataofx.employment.school.controller.ActivityController$MyTask@2280cdac rejected from java.util.concurrent.ThreadPoolExecutor@1517365b[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.dataofx.employment.school.controller.ActivityController.main(ActivityController.java:139)
task 0执行完毕
正在执行task 2
task 1执行完毕
正在执行task 3
task 4执行完毕
task 2执行完毕
task 3执行完毕
从控制台打印可以看出:线程池的执行顺序:
- 进来一个线程就开启一个核心线程
- 当核心线程占满后,进来的线程会放到等待队列中
- 当等待队列占满后,会放入到线程池的额外线程,直到达到最大线程数。
- 因为这里
线程池能容纳的最大线程数 = 最大线程数(3) + 等待队列(2) = 5
,但是这里做了6次循环,所以第六个线程进入线程池的时候被拒绝报错。
6.线程池的四种状态
- RUNNING:当创建线程池后,初始时,线程池处于
RUNNING
状态; - SHUTDOWN:如果调用了
shutdown()
方法,则线程池处于SHUTDOWN
状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕; - STOP:如果调用了
shutdownNow()
方法,则线程池处于STOP
状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务; - TERMINATED:当线程池处于
SHUTDOWN
或STOP
状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED
状态。
7. 如何设置线程池参数
核心线程数
线程核心数的设置理论公式:
CPU密集型任务,需尽量压缩线程数,参考值设置为N(CPU) + 1;
IO密集型任务,参考值可以设置为2*N(CPU);
示例:接口QPS为30,TP99为200ms,考虑系统抖动(QPS)
核心线程数:30 * 0.2 + 4(这个看各自系统的稳定性和网络波动)= 10
最大线程数 = 2 * 核心线程数
解释:
cpu 密集其实就是说这个请求进来之后一直在进行运算,计算量比较大,这时候为了避免cpu时间片切换线程执行,就根据cpu核心数 +1
密集型和上面 的区别就是他需要不停的访问三方或者数据库,一定会引起cpu时间片切换,此时设置为 cpu 核心数的两倍
QPS:一个接口1秒内被请求多少次,TP99:所有请求的99%都成功了此时的平均响应时间,系统抖动:各系统的稳定性以及网络波动
上面这个只是个理论值,实际可以在功能实现后进行压测测试,确定接口响应。上线后监控系统2-3天
8. submit 和 execute 区别
在Java中,线程池(ThreadPoolExecutor)提供了多种方法来提交任务并执行。其中,submit()方法和execute()方法是两种常用的提交任务的方式,它们的区别如下:
- 返回值类型:submit()方法返回一个Future对象,而execute()方法没有返回值。
- 异常处理:submit()方法可以通过Future对象来获取任务的执行结果、取消任务或处理任务抛出的异常;而execute()方法无法获取任务的返回结果或处理任务的异常,只能通过设置UncaughtExceptionHandler来处理任务内部抛出的异常。
- 可接收任务类型:submit()方法可以接收Callable任务和Runnable任务,而execute()方法只能接收Runnable任务。
总结起来,主要区别在于submit()方法能够获取任务执行的结果,可以更方便地处理任务的返回值和异常;而execute()方法则更为简洁,适用于不需要获取返回值或处理异常的场景。
下面是一个示例代码,展示了submit()和execute()方法的使用:
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 使用submit()提交Callable任务
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "Callable Task Result";
}
});
try {
String result = future.get();
System.out.println("Callable Task Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 使用execute()提交Runnable任务
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("Runnable Task Executed");
}
});
executorService.shutdown();
}
}
在上述示例中,我们首先使用submit()方法提交一个Callable任务,并通过Future对象的get()方法获取任务的返回结果。
接着,我们使用execute()方法提交一个Runnable任务,由线程池执行任务。
最后,我们调用线程池的shutdown()方法来关闭线程池。
总之,submit()方法在执行任务时提供了更多的控制和灵活性,适用于需要获取任务执行结果或处理异常的场景。而execute()方法则更为简洁,主要用于不需要关心任务结果或处理异常的情况。
9. 线程池使用规范
参考:Java 线程池规范