一、ThreadPoolExecutor应用
为什么使用线程池?答:降低线程创建和关闭的消耗。当我们想同时开启200个线程,自己手动创建线程,使用后再销毁,会导致资源占用过多,所以应考虑使用线程池。线程池里有很多提前备好的可用线程资源,如果需要就直接从线程池里拿;不用的时候,线程池会自动帮我们管理。
使用线程池主要有以下两个好处:
- 减少在创建和销毁线程上所花的时间以及系统资源的开销
- 不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存
JDK提供了一个类Executors,还有好多包装好的线程池,但是参数是固定的,所以常常要自己通过类ThreadPoolExecutor自定义线程池的属性。java提供的定义好的线程池:
FixedThreadPool
:创建固定大小的线程池,数量通过传入的参数决定。SingleThreadExecutor
:创建一个线程容量的线程池,所有的线程依次执行,相当于创建固定数量为 1 的线程池。CachedThreadPool
:创建可缓存的线程池,没有最大线程限制(实际上是 Integer.MAX_VALUE)。如果有空闲线程等待时间超过一分钟,就关闭该线程。ScheduledThreadPool
:创建计划 (延迟) 任务线程池, 线程池中的线程可以被设置为在特定的延迟时间之后开始执行,也可以以固定的时间重复执行(周期性执行)。SingleThreadScheduledExecutor
:创建单线程池延迟任务,创建一个线程容量的计划任务。
简单理解一下线程池的工作流:池内包括核心线程、非核心线程、阻塞队列、线程工厂。
- 当一个任务被分配给线程池,池会优先找到一个空闲的核心线程来完成这个任务。
- 如果没有空闲的核心线程,就创建或找到一个非核心线程来进行这个任务。一般我们会设置核心线程一直在池内存在,但非核心线程不会,当一个核心线程的空闲时间达到了我们设置的一个时间长度,该非核心线程就会被回收。
- 如果所有线程都在忙,并且已经达到了池内最大线程数的限制,那么就将此任务放入等待队列,等待线程空闲下来后,再来处理这些等待中的任务。
- 如果这三个地方都放满了,就要使用拒绝策略了,也就是以一定的策略来处理这个无处安放的任务。拒绝策略一般包括直接抛弃这个任务,或者将队头等待时间过长的一个任务删除,把当前新任务加入队列,后面会有详细介绍。
一个简单使用的例子:
ThreadPoolExecutor executor = new ThreadPoolExecutor( // 1.创建线程池对象
1, // 核心线程数
2, // 最大线程数量,即核心和非核心线程数量之和
500, // 非核心线程的最大空闲时间,查过这个时间,相应非核心线程就会被回收
TimeUnit.SECONDS, // 上一个参数的时间单位
new LinkedBlockingDeque<>(), // 阻塞队列设置
new ThreadFactory() {
// 生产线程的工厂,实现接口ThreadFactory
@Override // 重写创建新线程的方法
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("测试-"); // 可以自定义设置新线程的名字
return t;
}
},
new ThreadPoolExecutor.AbortPolicy() // 设置拒绝策略
);
executor.execute(new Runnable() {
// 2.调用execute或submit方法执行Runnable任务或Callable任务
@Override
public void run() {
System.out.println("love hy");
}
});
Future<Object> future = executor.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
return null;
}
});
future.get();
二、ThreadPoolExecutor核心参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数(非核心线程数)
long keepAliveTime, // 非核心线程的最大空闲时间
TimeUnit unit, // keepAliveTime的单位
BlockingQueue<Runnable> workQueue, // 等待队列,可以使用阻塞队列或工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) // 拒绝策略
1、核心线程数
核心线程在池中的数量,核心线程会一直在池中等待任务到来,即使空闲也会一直存在,除非设置了allowCoreThreadTimeOut,核心线程才会在空闲一段时间后被回收。
到来的任务会优先分配给核心线程执行。
2、最大线程数
例如此参数设置为3,核心线程数为2,等待队列最大容量为2,此时来了5个任务,2个在被核心线程执行,2个进入等待队列,还有一个会被非核心线程执行,最大线程数就规定了最多有几个核心和非核心线程
3、keepAliveTime
非核心线程不会一直存在,此参数就是非核心线程的最大空闲时间,超过这个时间,非核心线程就会消失
4、阻塞队列
最常用的是ArrayBlockingQueue、LinkedBlockingQueue
5、线程工厂
线程池创建后,默认是空的,任务进入线程池后,线程才被创建。线程工厂使得我们程序员可以控制对于线程的创建,从而方便后续对这些线程的操作(如通过线程名字找到线程对象)
6、拒绝策略
当核心线程、非核心线程都在运行,且等待队列已经满了的时候,又来了新任务,如何拒绝这个任务。RejectedExecutionHandler是一个接口,只有一个抽象方法rejectedExecution(Runnable r, ThreadPoolExecutor executor)
,实现类有四个。实现类AbortPolicy对rejectedExecution的实现直接抛出了一个异常,意思是线程池无法处理多出的这个任务。实现类CallerRunsPolicy对rejectedExecution的实现是让main线程执行这个新线程:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
实现类DiscardOldestPolicy重写的rejectedExecution方法将等待队列的队头弹出,将新线程推入等待队列:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll(); // 把等待队列对头弹出
e.execute(r); // 新线程加入线程池
}
}
实现类DiscardPolicy更绝,重写方法啥也不做:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
可以看出,拒绝策略比较好实现,业务中常常由程序员自己实现。
三、ThreadPoolExecutor执行流程
即业务线程提交任务到线程池后,任务的处理流程。
源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
// 判断当前池内的工作线程数是否小于核心线程数
// 如果小于,就创建核心线程并执行任务
if (addWorker(command, true))