目录
7、RejectedExecutionHandler 拒绝策略
一、什么是线程池?
线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待空闲状态。如果有新的线程任就分配一个空闲线程执行。如果所有线程都处于忙碌状态,线程池会创建一个新线程进行处理或者放入队列(工作队列)中等待。
二、线程池的常用方法
- 执行无返回值的线程任务:void execute(Runnable command);
- 提交有返回值的线程任务:Future<T>submit(Callable<T>task);
- 关闭线程池:void shutdown();或shutdownNow();
- 等待线程池关闭:boolean awaitTermination(long timeout,TimeUnit unit);
1、执行线程任务
execute()只能提交 Runnable 类型的任务,没有返回值,而 submit()既能提交 Runnable 类型任务也能提交 callable 类型任务,可以返回 Future 类型结果,用于获取线程任务执行结果。
execute()方法提交的任务异常是直接抛出的,而 submit()方法是捕获异常,当调用 Future 的 get()方法获取返回值时,才会抛出异常。
public class Demo01 {
public static void main(String[] args) {
// 创建了固定数目的线程池
ExecutorService executorService = Executors.newFixedThreadPool(30);
// 提交了10个打印字母线程任务
for (int i = 1; i <= 10; i++) {
executorService.execute(() -> {
for (char c = 'A'; c <= 'Z'; c++) {
System.out.println(Thread.currentThread().getName() + ":" + c);
}
});
}
// 提交了20次打印数字线程任务
for (int n = 1; n <= 20; n++) {
executorService.execute(() -> {
for (int i = 1; i <= 26; i++) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
这里就是创建了一个固定数目的线程池,循环的方式依次向线程池提交任务并让线程池分配线程来执行当前任务,这里会把A到Z依次打印10遍。
后面的也是一样的道理,打印20遍1到26的数字,并且每个线程执行完一个循环休眠200ms。
最后关闭线程池。
2、关闭线程池
线程池在程序结束的时候要关闭。使用 shutdown()方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()会立刻停止正在执行的任务;
当使用 awaitTermination() 方法时,主线程会处于一种等待的状态,按照指定的 timeout 检查线程池。
第一个参数指定的是时间,第二个参数指定的是时间单位(当前是秒)。返回值类型为 boolean 型。
- 如果等待的时间超过指定的时间,但是线程池中的线程运行完毕,awaitTermination()返回 true 。
- 如果等待的时间超过指定的时间,但是线程池中的线程未运行完毕awaitTermination()返回 false 。
- 如果等待时间没有超过指定时间,则继续等待。
该方法经常与 shutdown() 方法配合使用,用于检测线程池中的任务是否已经执行完毕。
// 线程池启动关闭操作
executor.shutdown();
// 判断线程池是否已经关闭
while (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
System.out.println("线程池尚未关闭!");
}
System.out.println(Ops.result);
当关闭线程后,通过awaitTermination判断在指定时间内,看线程池中的线程是否运行完毕,如果运行完毕,则不会打印信息,如果还有线程正在运行,则提示线程池尚未关闭。
三、线程池的执行流程
1、提交线程任务
提交一个新线程任务,线程池会在线程池中分配一个空闲线程,用于执行线程任务;
2、是否存在空闲线程
如果线程池中不存在空闲线程,则线程池会判断当前"存活的线程数"是否小于核心线程数 corePoolSize 。
- 如果小于核心线程数 corePoolsize,线程池会创建一个新线程(核心线程)去处理新线程任务;
- 如果大于核心线程数 corePoolsize,线程池会检查工作队列;
3、检查工作队列
- 如果工作队列(BlockingQueue)未满,则将该线程任务放入工作队列进行等待。线程池中如果出现空闲线程将从工作队列中按照FIFO的规则取出1个线程任务并分配执行;
- 如果工作队列已满,则判断线程数是否达到最大线程数 maximumPoolsize;
4、判断线程数是否达到最大线程数
- 如果当前"存活线程数"没有达到最大线程数 maximumPoolsize,则创建一个新线程(非核心线程)执行新线程任务;
- 如果当前"存活线程数"已经达到最大线程数 maximumPoolsize,直接采用拒绝策略处理新线程任务;
5、拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下五种策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务(即最旧的任务,也就是最早进入队列的任务),然后重新提交被拒绝的任务。
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。也就是谁调用的execute()方法谁来执行(谁调用,谁处理)。
- 自定义拒绝策略。
综上所述,执行顺序为:核心线程、工作队列、非核心线程、拒绝策略。
四、线程池的配置参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
ThreadPoolExecutor是JDK中的线程池实现,这个类实现了一个线程池需要的各个方法,它提供了任务提交、线程管理、监控等方法。共有7个参数:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。
1、corePoolSize 线程池核心线程数
也可以理解为线程池维护的最小线程数量,核心线程创建后不会被回收。大于核心线程数的线程,在空闲时间超过 keepAliveTime 后会被回收;
- 在创建了线程池后,默认情况下,线程池中并没有任何线程,当调用 execute()方法添加一个任务时,如果正在运行的线程数量小于 corePoolsize,则马上创建新线程并运行这个任务。
- IO密集计算:由于I/0设备的速度相对于CPU来说都很慢,所以大部分情况下,I/0 操作执行的时间相对于CPU 计算来说都非常长,这种场景我们一般都称为I/密集型计算。最佳线程数 = CPU核数*[ 1+( I/O耗时 / CPU 耗时 ) ]。
- CPU密集型:CPU密集型计算大部分场景下都是纯 CPU 计算,多线程主要目的是提升CPU 利用率,最佳线程数 = "CPU 核心数 +1"。这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以临时替补,从而保证CPU的利用率。
2、maximumPoolSize 线程池最大线程数
线程池允许创建的最大线程数;(包括核心线程池数量),也就是非核心线程+核心线程数量总和。
3、keepAliveTime 非核心线程线程存活时间
当一个可被回收的线程的空闲时间大于 keepAliveTime ,就会被回收。
- 当线程池中的线程数大于 corePoolsize 时,如果一个线程空闲的时间达到 keepAliveTime,则会被回收,直到线程池中的线程数不超过 corePoolsize。
- 如果设置 allowCoreThreadTimeOut = true ,在线程池中的线程数不大于 corePoolsize时,keepAliveTime 参数也会起作用,直到线程池中的线程数为0;
4、TimeUnit 时间单位
参数keepAliveTime的时间单位。
5、BlockingQueue 阻塞工作队列
用于存储等待执行的任务,如果有一些任务需要被执行,但是当前核心线程数以满且全部都在运行时期,则将当前任务存储在阻塞工作队列中,等核心线程执行完后,队列中的任务会进行分配执行。它仅仅用来存放被 execute() 方法提交的Runnable任务。工作队列实现了BlockingQueue接口。
JDK默认的工作队列有五种:
- ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。
- LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无解),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。
- SynchronousQueue 同步队列:容量为0,也就是说没有核心线程,直接使用非核心线程执行任务。添加任务必须等待取出任务,这个队列相当于通道,不存储元素。
- PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。
- DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。
6、ThreadFactory 线程工厂
用于创建线程,以及自定义线程名称,需要实现ThreadFactory接口;
默认线程工厂:
/**
* The default thread factory
*/
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
也可以自定义线程工厂:
// 自定义线程工厂
class MyThreadFactory implements ThreadFactory {
private String prefix = "";
private final AtomicInteger threadNumber = new AtomicInteger(0);
public MyThreadFactory(String bizName) {
prefix = bizName + "-线程:";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, prefix + threadNumber.incrementAndGet());
return t;
}
}
这里是通过runnable和名称对线程进行赋值创建,并返回给调用者。
7、RejectedExecutionHandler 拒绝策略
在上面的执行流程中已经提及到了。