[Java并发]线程池
ThreadPoolExecutor tpe = new ThreadPoolExecutor(
10,
20,
1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy());
线程池参数的含义
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
这 7 个参数分别是:
- corePoolSize:核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:空闲线程存活时间。
- TimeUnit:时间单位。
- BlockingQueue:线程池任务队列。
- ThreadFactory:创建线程的工厂。
- RejectedExecutionHandler:拒绝策略。
corePoolSize
核心线程数:是指线程池中长期存活的线程数。
这就好比古代大户人家,会长期雇佣一些“长工”来给他们干活,这些人一般比较稳定,无论这一年的活多活少,这些人都不会被辞退,都是长期生活在大户人家的。
maximumPoolSize
最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
这是古代大户人家最多可以雇佣的人数,比如某个节日或大户人家有人过寿时,因为活太多,仅靠“长工”是完不成任务,这时就会再招聘一些“短工”一起来干活,这个最大线程数就是“长工”+“短工”的总人数,也就是招聘的人数不能超过 maximumPoolSize。
最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize,否则在程序运行时会报 IllegalArgumentException 非法参数异常
keepAliveTime
空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数=maximumPoolSize(最大线程数)-corePoolSize(核心线程数)。
还是以大户人家为例,当大户人家比较忙的时候就会雇佣一些“短工”来干活,但等干完活之后,不忙了,就会将这些“短工”辞退掉,而 keepAliveTime 就是用来描述没活之后,短工可以在大户人家待的(最长)时间。
TimeUnit
时间单位:空闲线程存活时间的描述单位,此参数是配合参数 3 使用的。
参数 3 是一个 long 类型的值,比如参数 3 传递的是 1,那么这个 1 表示的是 1 天?还是 1 小时?还是 1 秒钟?是由参数 4 说了算的。
TimeUnit 有以下 7 个值:
- TimeUnit.DAYS:天
- TimeUnit.HOURS:小时
- TimeUnit.MINUTES:分
- TimeUnit.SECONDS:秒
- TimeUnit.MILLISECONDS:毫秒
- TimeUnit.MICROSECONDS:微妙
- TimeUnit.NANOSECONDS:纳秒
BlockingQueue
阻塞队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。
它可以设置以下几个值:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
比较常用的是 LinkedBlockingQueue,线程池的排队策略和 BlockingQueue 息息相关。
ThreadFactory
线程工厂:线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
线程工厂的使用示例如下:
public static void main(String[] args) {
// 创建线程工厂
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
// 创建线程池中的线程
Thread thread = new Thread(r);
// 设置线程名称
thread.setName("Thread-" + r.hashCode());
// 设置线程优先级(最大值:10)
thread.setPriority(Thread.MAX_PRIORITY);
//......
return thread;
}
};
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
threadFactory); // 使用自定义的线程工厂
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println(String.format("线程:%s,线程优先级:%d",
thread.getName(), thread.getPriority()));
}
});
}
RejectedExecutionHandler
拒绝策略:当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。
默认的拒绝策略有以下 4 种:
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
线程池的默认策略是 AbortPolicy 拒绝并抛出异常。
总结
- corePoolSize:核心线程数,线程池正常情况下保持的线程数,大户人家“长工”的数量。
- maximumPoolSize:最大线程数,当线程池繁忙时最多可以拥有的线程数,大户人家“长工”+“短工”的总数量。
- keepAliveTime:空闲线程存活时间,没有活之后“短工”可以生存的最大时间。
- TimeUnit:时间单位,配合参数 3 一起使用,用于描述参数 3 的时间单位。
- BlockingQueue:线程池的任务队列,用于保存线程池待执行任务的容器。
- ThreadFactory:线程工厂,用于创建线程池中线程的工厂方法,通过它可以设置线程的命名规则、优先级和线程类型。
- RejectedExecutionHandler:拒绝策略,当任务量超过线程池可以保存的最大任务数时,执行的策略。
线程池的工作过程
- 最开始,线程池中的没有线程在运行
- 向线程池提交任务,
- 线程池不断创建
核心线程来执行这些任务,即使有其他空闲线程能够执行新来的任务, 也会继续创建线程,直到核心线程数到达上限 - 继续向线程池提交任务
- 新提交的任务会保存在
阻塞任务队列,直到阻塞任务队列到达上限 - 继续向线程池提交任务,
- 线程池创建
非核心线程来执行这些任务,直到总线程数到达上限, - 继续向线程池提交任务,
- 线程池采用四种
拒绝策略,- 直接抛出异常,
- 直接拒绝执行,
- 删掉阻塞队列最前面的任务,让当前任务上去执行,
- 让调用者自己去执行
关闭线程池
关闭线程池是一个重要的操作,它确保线程池不再接受新的任务,并且在已提交的任务完成后,逐渐关闭所有线程。Java中的 ExecutorService 接口提供了两个方法来关闭线程池:
-
shutdown()方法:- 调用
shutdown()方法会平缓地关闭线程池。它将禁止新的任务被提交,但会等待已提交的任务(包括正在执行和在队列中的任务)执行完成。 - 一旦所有任务完成,线程池就会终止,且不再接受新的任务。
executorService.shutdown(); - 调用
-
shutdownNow()方法:- 调用
shutdownNow()方法会尝试立即停止所有正在执行的任务,并停止等待执行的任务。这可能会导致一些任务被中断,因为它们可能正在执行一些阻塞操作。 - 与
shutdown()不同,shutdownNow()不会等待任务执行完成,而是立即返回一个包含尚未执行的任务的列表。
List<Runnable> remainingTasks = executorService.shutdownNow(); - 调用
一般来说,首选使用 shutdown() 方法,因为它提供了平缓的关闭过程,允许已提交的任务得到充分的执行时间。只有在需要尽快停止并清理线程池时,才考虑使用 shutdownNow() 方法。
需要注意的是,一旦线程池关闭,就不能再向其提交新的任务。如果需要重新使用线程池,必须重新创建一个新的线程池实例。
为什么线程池不允许使用Executors去创建? 推荐方式是什么?
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
- newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
- newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
原文链接:https://pdai.tech/md/java/thread/java-thread-x-juc-executor-ThreadPoolExecutor.html
