它山之石,可以攻玉。借鉴整理线程池相关文章,以及自身实践。
文章目录
1. 线程池概述
前面介绍了 Java 线程用法,但是当任务量特别大且任务执行时间比较长的时候,创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率。而且,创建的线程无法进行统一管理。
例如:
创建线程消耗时间 T1,执行任务消耗时间 T2,销毁线程消耗时间 T3。
如果 T1+T3 > T2,那么是不是说开启一个线程来执行这个任务太不划算了!
正好,线程池缓存线程,可用已有的闲置线程来执行新任务,避免了 T1+T3 带来的系统开销。
线程池优点:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 可以根据系统的需求和硬件环境灵活的控制线程的数量,对所有线程进行统一的管理和控制,从而提高系统的运行效率。
2. 线程池UML架构
1. Executor
Executor
:线程池框架最基础的任务执行接口,Executor 框架中几乎所有类都直接或者间接实现 Executor 接口,该接口提供了一种将任务提交
与任务执行
分离开来的机制,该接口只有一个方法execute()
,用来执行已提交的线程任务。
public interface Executor {
void execute(Runnable command);
}
2. ExecutorService
继承于 Executor 接口,Java异步目标任务接口,对外提供异步任务的接收服务。扩展了对任务各种操作的接口,该接口是我们最常用的线程池接口。
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
3. AbstractExecutorService
抽象类,实现了ExecutorService,提供 ExecutorService 执行方法的默认实现,该类实现了submit、invokeAny、invokeAll等方法,并且提供了 newTaskFor 方法返回一个 RunnableFuture 对象。
4. ThreadPoolExecutor
线程池实现类,继承于AbstractExecutorService,JUC线程池的核心实现类。
5. ScheduledThreadPoolExecutor
继承于ThreadPoolExecutor,实现了 ExecutorService 中延时执行
和定时执行
等抽象方法。
6. Executors
静态工厂类,它通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池示例对象。
3. Executors创建线程的4种方法
Executors
工具类是 Executor 框架的一个工具帮助类,提供了4种创建线程池的方式,这4种方式都是直接或间接通过ThreadPoolExecutor
来实现的,一般情况下我们可以通过该工具类来创建线程池,如果该工具类的几个方法满足不了的情况下,我们可以自定义实现。
3.1 newSingleThreadExecutor
- public static ExecutorService
newSingleThreadExecutor()
- public static ExecutorService
newSingleThreadExecutor
(ThreadFactory threadFactory
)
newSingleThreadExecutor(ThreadFactory threadFactory)
源码:
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
阻塞队列:LinkedBlockingQueue
任务类 MyRunnable
/*
任务类,包含一个任务编号,在任务中,打印出是哪一个线程正在执行任务
*/
class MyRunnable implements Runnable {
private int id;
public MyRunnable(int id) {
this.id = id;
}
@Override
public void run() {
//获取线程的名称,打印一句话
String name = Thread.currentThread().getName();
System.out.println(name + "执行了任务..." + id);
}
}
ThreadPoolDemo示例:
public class ThreadPoolDemo {
public static void main(String[] args) {
test1();
test2();
}
private static void test1() {
//1:使用工厂类获取线程池对象
ExecutorService es = Executors.newSingleThreadExecutor();
//2:提交任务;
for (int i = 1; i <= 5; i++) {
es.submit(new MyRunnable(i));
}
}
private static void test2() {
//1:使用工厂类获取线程池对象
ExecutorService es = Executors.newSingleThreadExecutor(new ThreadFactory() {
int n = 1;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "自定义的线程名称-" + n++);
}
});
//2:提交任务;
for (int i = 1; i <= 5; i++) {
es.submit(new MyRunnable(i));
}
}
}
特点:
- 只有一个线程的线程池
- 单线程线程池中的任务是按照提交的次序顺序执行的
- 池中的唯一线程的存活时间是无限的
- 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列
LinkedBlockingQueue
是无界的。
适用场景:
- 任务按照提交次序,一个任务一个任务地逐个执行的场景
3.2 newFixedThreadPool
- public static ExecutorService
newFixedThreadPool(int nThreads)
- public static ExecutorService
newFixedThreadPool
(int nThreads
,ThreadFactory threadFactory
)
newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
源码:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
阻塞队列:LinkedBlockingQueue
ThreadPoolDemo示例:
public class ThreadPoolDemo {
public static void main(String[] args) {
test1();
test2();
}
private static void test1() {
//1:使用工厂类获取线程池对象
ExecutorService es = Executors.newFixedThreadPool(3);
//2:提交任务;
for (int i = 1; i <= 5; i++) {
es.submit(new MyRunnable(i));
}
}
private static void test2() {
//1:使用工厂类获取线程池对象
ExecutorService es = Executors.newFixedThreadPool(3, new ThreadFactory() {
int n = 1;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "自定义的线程名称-" + n++);
}
});
//2:提交任务;
for (int i = 1; i <= 5; i++) {
es.submit(new MyRunnable(i));
}
}
}
特点:
- 如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
- 线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列
LinkedBlockingQueue
(无界)。
适用场景:
- 需要任务长期执行的场景
- CPU密集型任务
缺点:
- 内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。
3.3 newCachedThreadPool
- public static ExecutorService
newCachedThreadPool()
- public static ExecutorService
newCachedThreadPool
(ThreadFactory threadFactory
)
newCachedThreadPool(ThreadFactory threadFactory)
源码:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
默认空闲 60秒 线程回收。阻塞队列:SynchronousQueue
ThreadPoolDemo示例:
public class ThreadPoolDemo {
public static void main(String[] args) {
test1();
test2();
}
private static void test1() {
//1:使用工厂类获取线程池对象
ExecutorService es = Executors.newCachedThreadPool();
//2:提交任务;
for (int i = 1; i <= 5; i++) {
es.submit(new MyRunnable(i));
}
}
private static void test2() {
//1:使用工厂类获取线程池对象
ExecutorService es = Executors.newCachedThreadPool(new ThreadFactory() {
int n = 1;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "自定义的线程名称-" + n++);
}
});
//2:提交任务;
for (int i = 1; i <= 5; i++) {
es.submit(new MyRunnable(i));
}
}
}
特点:
- 在接收新的异步任务 target 执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
- 不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。
适用场景:
- 需要快速处理突发性强、耗时较短的任务场景,如 Netty 的 NIO 处理场景、REST API接口的瞬时削峰场景
缺点:
- 线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。
3.4 newScheduledThreadPool
- public static ScheduledExecutorService
newScheduledThreadPool(int corePoolSize)
- public static ScheduledExecutorService
newScheduledThreadPool
(int corePoolSize
,ThreadFactory threadFactory
)
ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
源码:
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
阻塞队列:DelayedWorkQueue
任务类 MyRunnable2:
class MyRunnable2 implements Runnable {
private int id;
public MyRunnable2(int id) {
this.id = id;
}
@Override
public void run() {
//获取线程的名称,打印一句话
String name = Thread.currentThread().getName();
String curTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
System.out.println(name + " " + curTime + " " + "执行了任务..." + id);
}
}
ThreadPoolDemo示例:
public class ThreadPoolDemo {
public static void main(String[] args) {
test1();
test2();
test3();
}
private static void test1() {
//1:使用工厂类获取线程池对象
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
//2:提交任务;
for (int i = 1; i <= 5; i++) {
ses.submit(new MyRunnable(i));
}
}
private static void test2() {
//1:使用工厂类获取线程池对象
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3, new ThreadFactory() {
int n = 1;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "自定义的线程名称-" + n++);
}
});
//2:提交任务;
for (int i = 1; i <= 5; i++) {
ses.submit(new MyRunnable(i));
}
}
private static void test3() {
//1:使用工厂类获取线程池对象
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
//2:提交任务;
for (int i = 1; i <= 5; i++) {
ses.scheduleAtFixedRate(new MyRunnable2(0), 0, 5, TimeUnit.SECONDS);
}
}
}
小结
Executors创建线程池的4种方法十分方便,但是构造器创建普通线程池、可调度线程池比较复杂,这些构造器会涉及大量的复杂参数,已经较少使用。而且4种方式创建的线程池均存在缺点:
newFixedThreadPool
、newSingleThreadExecutor
:阻塞队列无界,队列很大时,堆积大量任务会导致OOM(内存耗尽)。newCachedThreadPool
、newScheduledThreadPool
: 线程数量无上界,会导致创建大量的线程,从而导致OOM,甚至导致CPU线程资源耗尽。
Java 通过 Executors 提供了四种线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor
的参数实现的。
通常建议直接使用线程池ThreadPoolExecutor
的构造器
4. 线程池标准创建方式-ThreadPoolExecutor
public class ThreadPoolExecutor extends AbstractExecutorService {
private volatile int corePoolSize;//核心线程数,即使线程空闲也不会被收回
private volatile int maximumPoolSize;//线程的上限
private volatile long keepAliveTime;//线程的最大空闲时长
private final BlockingQueue<Runnable> workQueue;//任务的排队队列
private volatile ThreadFactory threadFactory;//新线程的产生方式
private volatile RejectedExecutionHandler handler;//拒绝策略
}
4.1 线程池参数
1. corePoolSize 核心线程数
- 核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
- 如果指定 ThreadPoolExecutor 的
allowCoreThreadTimeOut
属性为true
,那么核心线程如果闲置状态的话,超过一定时间(keepAliveTime
),就会被销毁掉。 - 线程池接收到新任务,当前工作线程数少于 corePoolSize,即使有空闲的工作线程,也会创建新的线程来处理该请求,直到线程数达到corePoolSize。
2. maximumPoolSize 最大线程数量
- 当前工作线程数多于 corePoolSize 数量,但小于 maximumPoolSize 数量,那么仅当任务排队队列已满时才会创建新线程。
- 如果 maximumPoolSize 被设置为无界值(如
Integer.MAX_VALUE
)时,线程池可以接收任意数量的并发任务。
3. keepAliveTime 空闲线程存活时间
- 该线程池中非核心线程闲置超时时长
- 一个非核心线程,如果闲置状态的时长超过这个参数所设定的时长,就会被销毁掉。如果设置
allowCoreThreadTimeOut = true
,则会作用于核心线程。
4. TimeUnit 空闲线程存活时间单位
- keepAliveTime 的单位,TimeUnit是一个枚举类型。MILLISECONDS(毫秒)、SECONDS (秒)等
5. BlockingQueue workQueue 任务队列
- 维护着等待执行的 Runnable 异步任务。
- 当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。
6. threadFactory 线程工厂
- 创建线程的方式,实现方法
public Thread newThread(Runnable r)
即可。
7. RejectedExecutionHandler handler 拒绝策略
- workQueue 队列已满,总线程数又达到了 maximumPoolSize,执行拒绝策略。
4.2 参数执行流程
- 线程数量未达到
corePoolSize
,则新建一个线程(核心线程)执行任务。 - 线程数量达到了
corePoolSize
,则将任务移入队列workQueue
等待。 - 队列已满,新建线程(非核心线程)执行任务。
- 队列已满,总线程数又达到了
maximumPoolSize
,执行拒绝策略RejectedExecutionHandler
抛出异常。 - 非核心线程闲置状态时长超过
keepAliveTime
,就会被销毁掉。
示意图:
4.3 参数设置
首先,需要明白3个系统参数:
- tasks:每秒需要处理的任务数量
- tasktime:处理一个任务所需要的时间
- responsetime:系统允许任务最大的响应时间
1. corePoolSize
每个任务需要 tasktime 秒处理,则每个线程每秒可处理 1/tasktime 个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime)。即tasks*tasktime
个线程数。具体数字最好根据 8020原则,既按照80%的情况设计核心线程数,剩下的20%可以利用最大线程数处理。
2. workQueue
任务队列的长度要根据核心线程数、系统对任务响应时间的要求有关,队列长度可以设置为(corePoolSize/tasktime)*responsetime
。
3. maximumPoolSize
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定。最大线程数=(最大任务数-任务队列长度)*单个任务执行时间
。
4. keepAliveTime
keepAliveTiime 设定值可根据 任务峰值持续时间 来设定。
5. RejectedExecutionHandler
选择何种策略去执行,还得结合具体的业务场景。实际工作中,一般使用默认的AbortPolicy
策略,不允许失败场景使用CallerRunsPolicy
策略。
举个例子:
处理一个任务需要0.1秒,即 tasktime = 0.1s
系统80%的时间每秒都会产生100个任务,即 tasks = 100
系统每秒最大产生的任务是1000个
系统允许任务最大的响应时间为2s
corePoolSize = tasks*tasktime = 100*0.1 = 10
workQueue = (corePoolSize/tasktime)*responsetime = (10/0.1)*2 = 200
maximumPoolSize = (最大任务数-任务队列长度)*tasktime = (1000-200)*0.1 = 80
注:以上关于线程数量的计算并没有考虑 CPU 的情况。若结合 CPU 的情况,比如,当线程数量达到50时,CPU达到100%,则将maxPoolSize设置为60也不合适,此时若系统负载长时间维持在每秒1000个任务,则超出线程池处理能力,应设法降低每个任务的处理时间(tasktime)。
4.4 线程池提交任务的两种方式
1. execute方法
void execute(Runnable command)
:Executor接口方法,接收 Runnable 类型对象。
2. submit方法
<T> Future<T> submit(Callable<T> task)
:ExecutorService接口方法,接收 Callable 类型对象,Callable入参允许任务返回值。<T> Future<T> submit(Runnable task, T result)
:ExecutorService接口方法,接收 Runnable 类型对象。Future<?> submit(Runnable task)
:ExecutorService接口方法,接收 Runnable 类型对象。
区别:
execute()
方法只能接收 Runnable 类型的参数,而submit()
方法可以接收 Callable、Runnable 两种类型的参数。- Callable 类型的任务是可以返回执行结果的,而 Runnable 类型的任务不可以返回执行结果。
submit()
提交任务后会有返回值,而execute()
没有。submit()
方便 Exception 处理execute()
方法在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。而通过submit()
方法返回的 Future 对象(异步执行实例),可以进行异步执行过程中的异常捕获。
4.5 ThreadFactory(线程工厂)
public interface ThreadFactory {
Thread newThread(Runnable r);
}
ThreadFactory
是 Java 线程工厂接口,只有1个方法,调用ThreadFactory的唯一方法newThread()
创建新线程时,可以更改所创建的新线程的名称、线程组、优先级、守护进程状态等。
上述《3. Executors创建线程的4种方法》中,4种方法均可指定ThreadFactory。
Executors为线程池工厂类,用于快捷创建线程池(Thread Pool);ThreadFactory为线程工厂类,用于创建线程(Thread)
4.6 任务阻塞队列
维护着等待执行的 Runnable 对象。一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒。
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。常用的workQueue类型:
LinkedBlockingQueue
:一个基于链表实现的阻塞队列,按 FIFO 排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE
作为容量(无界队列)。ArrayBlockingQueue
:一个数组实现的有界阻塞队列(有界队列),队列中的元素按 FIFO 排序,ArrayBlockingQueue在创建时必须设置大小。接收到任务的时候,如果没有达到 corePoolSize 的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了 maximumPoolSize,并且队列也满了,则执行拒绝策略。SynchronousQueue
:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它。PriorityBlockingQueue
:是具有优先级的无界队列。DelayQueue
:队列内元素必须实现 Delayed 接口,这就意味着你传进去的任务必须先实现 Delayed 接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。
4.7 线程池的拒绝策略
public class RejectedExecutionException extends RuntimeException
1. AbortPolicy:拒绝策略
新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略。
必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
//1:使用线程池对象
int corePoolSize = 2;
int maximumPoolSize = 5;
long keepAliveTime = 60;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(10);
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, handler);
//2:提交任务;
for(int i=0; i<100; i++) {
try {
poolExecutor.execute(new MyRunnable(i));
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
}
结果看出,总共会执行100次任务,但有些任务会抛异常。
poolExecutor.execute()
提交任务,由于会抛出 RuntimeException,如果没有 try…catch 处理异常信息的话,会中断调用者的处理流程,后续任务得不到执行(跑不完100个)。
2. DiscardPolicy:抛弃策略
新任务就会直接被丢掉,并且不会有任何异常抛出。
...
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
...
总共执行任务不足100,大量任务直接被丢弃。
3. DiscardOldestPolicy:抛弃最老任务策略
将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列(一般队头元素最老)。
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
总共执行任务不足100,少量任务被丢弃。
4. CallerRunsPolicy:调用者执行策略
新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
总共会执行100次任务,部分任务main线程自己执行,输出main执行了任务...15
。
适应场景:一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。
5. 自定义策略
实现 RejectedExecutionHandler 接口的rejectedExecution()
方法。
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
//1:使用线程池对象
int corePoolSize = 2;
int maximumPoolSize = 5;
long keepAliveTime = 60;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(10);
//2:使用自定义拒绝策略,入参ArrayList
ArrayList<String> list = new ArrayList<>();
RejectedExecutionHandler handler = new MyRejectedPolicy(list);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, handler);
//3:提交任务;
for (int i = 1; i <= 100; i++) {
poolExecutor.execute(new MyRunnable(i));
}
//4:打印ArrayList内具体拒绝任务
Thread.sleep(2000);
System.out.println(list);
}
static class MyRejectedPolicy<T extends List> implements RejectedExecutionHandler {
public T t;
public MyRejectedPolicy(T t) {
this.t = t;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//被拒绝的任务写入List集合,当然也可以写入文件、数据库等等
System.out.println(executor.getTaskCount());
t.add(executor.getTaskCount());
}
}
}
6. 第三方实现的拒绝策略
- dubbo中的线程拒绝策略:输出了一条警告级别的日志,输出当前线程堆栈详情,继续抛出拒绝执行异常。
- Netty中的线程池拒绝策略:Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。
- activeMq中的线程池拒绝策略:activeMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常。
小结:
四种拒绝策略是相互独立无关的,选择何种策略去执行,还得结合具体的业务场景。实际工作中,一般直接使用 ExecutorService 的时候,都是使用的默认的 defaultHandler ,也即 AbortPolicy 策略。
4.8 调度器的钩子方法
三个钩子方法存在于 ThreadPoolExecutor 类,这3个方法都是空方法,一般会在子类中重写。
- protected void
beforeExecute
(Thread t, Runnable r) { }: 任务执行之前的钩子方法 - protected void
afterExecute
(Runnable r, Throwable t) { }: 任务执行之后的钩子方法 - protected void
terminated()
{ }: 线程池终止时的钩子方法
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
//1:使用线程池对象
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)){
@Override
protected void terminated()
{
System.out.println("调度器已停止...");
}
@Override
protected void beforeExecute(Thread t,Runnable target)
{
System.out.println("前钩执行...");
super.beforeExecute(t, target);
}
@Override
protected void afterExecute(Runnable target,Throwable t)
{
System.out.println("后钩执行...");
super.afterExecute(target, t);
}
};
//2:提交任务;
for (int i = 1; i <= 5; i++) {
poolExecutor.submit(new MyRunnable(i));
}
}
}
5. ScheduledExecutorService
ScheduledExecutorService
既有线程池的特性,也可以实现循环或延迟任务。
ScheduledExecutorService
是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。
ScheduledExecutorService本身提供了以下4个方法:
- ScheduledFuture<?>
schedule
(Runnable command
, long delay, TimeUnit unit):延迟 delay 单位时间后,执行一次任务。 - ScheduledFuture
schedule
(Callable callable
, long delay, TimeUnit unit):延迟 delay 单位时间后,执行一次任务。 - ScheduledFuture<?>
scheduleAtFixedRate
(Runnable command
, long initialDelay, long period, TimeUnit unit):固定速率。延迟 initialDelay 单位时间后,执行一次任务,之后每隔period
单位时间执行一次任务。 - ScheduledFuture<?>
scheduleWithFixedDelay
(Runnable command
, long initialDelay, long delay, TimeUnit unit):固定延时。延迟 initialDelay 单位时间后,执行一次任务,之后固定每隔delay
单位时间执行一次任务。
ScheduledExecutorService 和 Timer 进行对比,两者所提供的方法是类似的,区别在于 Timer 有提供指定时间点执行任务,而 ScheduledExecutorService 没有提供。
1. schedule
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
test1();
}
private static void test1() {
//1:使用工厂类获取线程池对象
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
//2:提交任务;
for (int i = 1; i <= 3; i++) {
ses.schedule(new MyRunnable2(i), 5, TimeUnit.SECONDS);
}
}
}
任务执行时间为初始化完成后 5s 才开始执行,且只执行一次。
2. scheduleAtFixedRate
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
test1();
}
private static void test1() {
//1:使用工厂类获取线程池对象
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
//2:提交任务;
for (int i = 1; i <= 3; i++) {
ses.scheduleAtFixedRate(new MyRunnable2(i), 5, 2, TimeUnit.SECONDS);
}
}
}
以上一个任务开始的时间计时,比如period
为2,那2秒后,检测上一个任务是否执行完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行。
如果你的任务执行时间超过2秒,那么任务时间间隔参数将无效,任务会不停地循环执行,由此可得出该方法不能严格保证任务按一定时间间隔执行。
MyRunnable2新增睡眠5s
class MyRunnable2 implements Runnable {
private int id;
public MyRunnable2(int id) {
this.id = id;
}
@Override
public void run() {
//获取线程的名称,打印一句话
String name = Thread.currentThread().getName();
String curTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
System.out.println(name + " " + curTime + " " + "执行了任务..." + id);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
因为,任务处理时间5s大于延时时间2s,故上一个任务执行完毕,则当前任务立即执行。
3. scheduleWithFixedDelay
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
test1();
}
private static void test1() {
//1:使用工厂类获取线程池对象
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
//2:提交任务;
for (int i = 1; i <= 3; i++) {
ses.scheduleWithFixedDelay(new MyRunnable2(i), 5, 2, TimeUnit.SECONDS);
}
}
}
固定延时是当任务执行耗时过长,超出设定的 delay 时间单位,后续的任务将会被顺延推迟。例子中,任务处理时间 5s 大于延时时间 2s,上一个任务执行完毕,当前任务会等待间隔时间 2s 后再执行。
6. 同步调用、异步调用
线程池接收到任务后,默认是异步执行,线程间独立执行,没有顺序。
那怎么实现按照顺序同步执行呢?
定义单线程池,单线程线程池中的任务是按照提交的次序顺序执行的.
//1:使用工厂类获取线程池对象
ExecutorService es = Executors.newSingleThreadExecutor();
//2:提交任务;
for (int i = 1; i <= 5; i++) {
es.submit(new MyRunnable(i));
}
7. 线程池的关闭
shutdown()
:等待当前工作队列中的剩余任务全部执行完成之后,才会执行关闭,但是此方法被调用之后,线程池不会再接收新的任务。shutdownNow()
:立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。awaitTermination()
:等待线程池完成关闭,shutdown()
、shutdownNow()
方法之后,用户程序都不会主动等待线程池关闭完成。
8. 面试题
问题:动态线程池有什么意义?
以前的传统软件当中,单机部署,硬件部署,确实,我们能使用的线程数取决于服务器的核心线程数,而且基本没有其他服务来争抢这些线程。
但是现在是容器的时代,云原生的时代。
多个容器部署在一个宿主机上,那么当高峰期的时候,某个容器就需要占用大量的cpu资源,如果所有的容器都将大部分资源占据,那么这个容器必然面临阻塞甚至瘫痪的风险。
当高峰期过了,释放这部分资源可以被释放掉,用于其他需要的容器。。
再结合到目前的云服务器节点扩容,都是需要动态扩缩容的的,和线程相似,在满足高可用的情况下,尽量的节约成本。
问题:线程池队列满了,你该怎么办
可以根据业务需求选择合适的拒绝策略,也通过实现 RejectedExecutionHandler 接口来自定义拒绝策略。比如把线程池无法执行的任务信息持久化写入数据库去,后台专门启动一个线程,后续等待线程池的工作负载降低了,这个后台线程就可以慢慢的从磁盘里读取之前持久化的任务重新提交到线程池。
问题:线上机器宕机,线程池阻塞队列中的请求怎么办?
我们知道队列里任务请求如果只在内存,没有持久化的话,机器宕机队列中的请求肯定会丢失。那么是不是可以在任务提交到线程池前,先把任务信息插入到数据库加个状态字段:未提交,当任务提交到线程池后,再更新状态为已提交,任务执行完之后再更新状态为已完成。机器宕机系统重启后,启动一个后台线程去读取未提交和已提交的任务信息,重新提交到线程池,继续执行任务。