1 ThreadPoolExecutor简介
1.1 应用场景
在Java的并发编程中,我们可以显示创建线程来并发处理任务,示例如下:
new Thread(() -> {
int i = 1;
System.out.println("线程开始工作...");
while (!isOver) {
try {
Thread.sleep(100);
System.out.println("线程执行第" + i + "次");
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程结束工作");
}).start();
但为了合理控制创建线程的数量和优化资源开销,可以通过线程池提供线程资源,不在业务代码中显示创建线程。使用线程池创建线程的优点如下:
- 使线程的创建更加规范,可以合理控制开辟线程的数量;
- 线程的细节管理交给线程池处理;
- 在执行大量异步任务时,由于减少了每个任务的调用开销,再加上线程池提供了一种限制和管理资源和线程的方法,能够明显提升性能;
- 方便统计信息,每个ThreadPoolExecutor可以保存一些基本的统计信息,例如完成的任务数量。
1.2 构造方法
ThreadPoolExecutor的4种构造方法如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数解析如下:
参数名 | 类型 | 定义 |
corePoolSize | int | 核心线程池大小。 |
maximumPoolSize | int | 最大线程池大小。 |
keepAliveTime | long | 线程的最大空闲时间。 |
unit | TimeUnit | 等待时间单位。 |
workQueue | BlockingQueue<Runnable> | 线程阻塞队列。 |
threadFactory | ThreadFactory | 线程创建工厂。 |
handler | RejectedExecutionHandler | 线程拒绝策略。 |
通常,核心和最大的线程池大小仅在构建线程池时设置,但也可以使用setCorePoolSize
和setMaximumPoolSize
方法动态修改。
1.3 预定义线程池
为简化线程池的使用方法,JDK提供了一些预定义线程池,减少了构造方法参数。3种常见预定义线程池的构造方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3种预定义线程池的用法和对比如下:
线程池 | 描述 | 线程池大小 | 等待时间 | 阻塞队列 | 适用场景 |
FixedThreadPool | 无界线程池,自动线程回收 | 线程全为核心线程,是固定大小的线程池。 | keepAliveTime参数对核心线程无效。 | workQueue为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE,如果任务提交速度持续大于任务处理速度,会造成大量任务阻塞,可能在执行拒绝策略前,内存溢出。 | 可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。 |
CachedThreadPool | 固定大小的线程池 | 无核心线程池,队列大小为Integer.MAX_VALUE,是缓存线程池,线程会消费。 | 线程等待60s后自动结束。 | workQueue为 SynchronousQueue同步队列,入队出队必须同时传递,且线程创建无限制。 | 快速处理大量耗时较短的任务,如Netty的NIO接受请求。 |
SingleThreadExecutor | 单一后台线程 | 单例线程池,只有1个核心线程。 | 同FixedThreadPool。 | 同FixedThreadPool。 | 常用于需要单例线程处理的场景。 |
需要注意的是,线程池最好不要使用Executors去创建,而要使用ThreadPoolExecutor。主要原因如下:
- JDK中的Executor框架虽然提供了
newFixedThreadPool()
、newSingleThreadExecutor()
和newCachedThreadPool()
等预定义线程池,但都有局限性,不够灵活; - 由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于明确线程池的运行规则,创建符合业务场景需要的线程池,避免资源耗尽的风险。
2 简单应用
在实际应用中,ThreadPoolExecutor线程池的创建、线程执行顺序和数量限制有以下特点:
- 进入核心线程池的线程将立即开始执行;
- 线程数超过核心线程池大小后,后续线程优先进入阻塞队列,阻塞队列满后,再进入线程池开始执行,直到数量到达最大线程池大小;
- 阻塞队列满后,线程池还能接收的线程数量为:最大线程池大小 - 核心线程池大小;
- 线程池能容纳的最大线程数量为:最大线程池大小 + 阻塞队列容量,线程数量超过该值后,后续加入的线程将触发拒绝策略;
- allowCoreThreadTimeOut的默认值为false,调用
allowCoreThreadTimeOut(true)
方法后,超过线程最大空闲时间的线程将被销毁。
任务类示例代码如下:
public class WorkerThread implements Runnable {
private int id;
public WorkerThread(int id) {
this.id = id;
}
@Override
public void run() {
try {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
System.out.println(format.format(new Date()) + " | The thread " + id + " starts working.");
Thread.sleep(3000);
System.out.println(format.format(new Date()) + " | The thread " + id + " is down.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试代码如下:
public class SyncTest {
/**
* 创建线程池
*/
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 100, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
for (int i = 0; i < 9; i++) {
try {
// 向线程池中添加任务
executor.execute(new WorkerThread(i));
System.out.println(format.format(new Date()) +
" | The thread " + i + " joined the thread pool.");
Thread.sleep(100);
} catch (RejectedExecutionException re) {
System.out.println(format.format(new Date()) + " | The thread " + i + " was rejected.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executor.shutdown();
}
}
输出结果见下图:
由输出结果可知:
- 在上述代码中,线程池最多可接收3 + 3 = 6个线程,共有9个线程希望加入;
- 核心线程池大小为2,线程0和1直接进入核心线程池开始执行;
- 阻塞队列大小为3,线程2、3、4共3个线程将依次进入阻塞队列开始等待;
- 最大线程池大小为3,阻塞队列满后,线程池还能接收的线程数量为3 - 2 = 1,所以线程5将进入线程池并开始执行;
- 当线程池已满时,后续加入的3个线程将触发拒绝策略,线程6、7、8将被直接拒绝,无法执行。
- 出现结束运行的线程之后,将开始处理阻塞队列中的等待线程,由于使用的是LinkedBlockingQueue,依照先进先出规则,先执行最早开始等待的线程,按线程2、3、4的顺序执行。
需要注意的有以下几点:
- 如果核心线程数量大于0,线程池在创建后,会一直存在,直到调用
shutdown()
方法,才会关闭线程池,所以忘记关闭线程池可能会导致内存溢出; - 如果程序中不再持有线程池的引用,并且线程池中没有线程时,线程池将会自动关闭,如果希望确保即使忘记调用
shutdown()
方法,也可以回收未引用的线程池,那么必须通过设置适当的keep-alive times并设置allowCoreThreadTimeOut(true)
,或设置核心线程数量为0; - ExecutorService是ThreadPoolExecutor的顶层接口,使用线程池中的线程执行每个提交的任务,我们可以使用Executors的工厂方法来创建ExecutorService。
下面我们将继续介绍线程池的一些进阶应用方法。
3 核心线程预启动
在默认情况下,初始化一个线程池后,只有当新任务到达时,线程池才开始创建和启动核心线程,但我们可以使用 prestartCoreThread()
和 prestartAllCoreThreads()
方法动态调整,预启动核心线程。
如果使用非空队列(如DelayQueue)构建线程池,则可能必须要预启动核心线程。
方法 | 作用 |
prestartCoreThread() | 创一个空闲任务线程,等待任务到达。 |
prestartAllCoreThreads() | 创建核心线程池数量的空闲任务线程,等待任务到达。 |
4 阻塞队列
阻塞队列BlockingQueue主要有以下几种类型:
BlockingQueue
LinkedBlockingQueue
ArrayBlockingQueue
DelayQueue
PriorityBlockingQueue
SynchronousQueue
在线程池的应用中,通常会使用LinkedBlockingQueue或ArrayBlockingQueue作为线程等待的队列,不过当遇到有特殊需求的场景时,也会用到其他阻塞队列。
SynchronousQueue是Direct handoffs直接握手队列,LinkedBlockingQueue和PriorityBlockingQueue可以作为Unbounded queues 无界队列,也可以作为Bounded queues 有界队列,ArrayBlockingQueue是有界队列。
4.1 LinkedBlockingQueue
LinkedBlockingQueue是基于链表的阻塞队列,其内部维护着一个由一个链表构成的数据缓冲队列。LinkedBlockingQueue具有以下特点:
- 当队列缓冲区达到缓存容量的最大值时,会阻塞生产者线程,直到从队列中消费掉一些数据,生产者线程才被唤醒;
- 队列容量大小默认为Integer.MAX_VALUE,类似无限大,所以如果不指定队列容量,当生产速度大于消费速度时,可能出现内存溢出;
- 队列中的线程是先进先出的,先执行最早开始等待的线程;
- 对于生产者和消费者,分别采用独立的锁来控制数据同步,意味着在高并发的情况下,生产者和消费者可并行操作队列中的数据,提高了队列的并发性能。
简单应用示例中使用的就是LinkedBlockingQueue阻塞队列。
4.2 ArrayBlockingQueue
ArrayBlockingQueue是基于数组的阻塞队列,其内部维护了一个定长数组来缓存队列中的数据对象。另外,队列内部还保存着两个整形变量,分别标识队列的头部和尾部在数组中的位置。
ArrayBlockingQueue和LinkedBlockingQueue的区别如下:
- ArrayBlockingQueue在生产和消费数据时共用同一个锁对象,意味着二者无法真正并行;
- ArrayBlockingQueue在插入或删除元素时,不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象,在需要长时间高效并发地处理大量数据的系统中,对于GC的影响还是存在一定的区别;
- 在创建ArrayBlockingQueue时,可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
ArrayBlockingQueue的构造函数如下:
public ArrayBlockingQueue(int capacity, boolean fair) {
...
}
capacity为队列大小,fair为是否采用公平锁,默认为false,该参数可省略。
4.3 DelayQueue
使用DelayQueue时,执行获取操作后,只有当一个元素的指定延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作永远不会被阻塞,只有获取数据的操作才会被阻塞。
DelayQueue使用场景较少,比如管理一个超时未响应的网络连接队列。
延迟任务类示例代码如下:
public class WorkerThread implements Runnable, Delayed {
private int id;
private long time;
private TimeUnit unit;
public WorkerThread(int id, long timeout, TimeUnit unit) {
this.id = id;
// 设置任务开始执行时间
this.time = unit.convert(timeout, unit) + System.currentTimeMillis();
this.unit = unit;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(time - System.currentTimeMillis(), unit);
}
@Override
public int compareTo(Delayed o) {
if (this.getDelay(unit) < o.getDelay(unit))
return -1;
else if (this.getDelay(unit) > o.getDelay(unit))
return 1;
else
return 0;
}
@Override
public void run() {
try {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
System.out.println(format.format(new Date()) + " | The thread " + this.id + " starts working.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
延迟任务类需要同时实现Runnable
和Delayed
接口,Delayed
接口中需要重写的两个关键方法如下:
方法 | 参数定义 | 返回值 | 描述 |
long getDelay(TimeUnit unit) | 时间单位 | 剩余延时时间 | 返回与此对象相关的剩余延迟时间。 |
int compareTo(Delayed o) | 比较对象 | 比较结果 | 制定选择取出对象的比较规则。 |
测试代码如下:
public class SyncTest {
private static DelayQueue queue = new DelayQueue<>();
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 100, TimeUnit.MILLISECONDS,
queue,
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
// 预启动所有核心线程
executor.prestartAllCoreThreads();
for (int i = 0; i < 3; i++) {
System.out.println(format.format(new Date()) + " | The thread " + i + " joined in the queue.");
try {
executor.execute(new WorkerThread(i, (i + 1) * 1000, TimeUnit.MILLISECONDS));
} catch (RejectedExecutionException re) {
System.out.println(format.format(new Date()) + " | The thread " + i + " was rejected.");
}
}
executor.shutdown();
}
}
在上述代码中,加入延迟队列的3个任务的延时分别为1s、2s、3s,输出结果如下:
线程池延迟队列的使用有2点需要注意:
- 先创建延迟队列,再创建线程池,不能直接在创建线程池时直接new一个延迟队列,会报错;
- 开始向线程池中添加延迟任务前,需要先预启动所有核心线程或将线程池中的线程数添加至最大核心线程数,保证后续添加的任务会进入延迟队列,否则进入核心线程池的任务会被立即执行;
- 可以将核心线程数设为0,这样就不用预启动核心线程了,所有进入线程池的任务将直接进入延迟队列,如:
ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 5, 100, TimeUnit.MILLISECONDS, queue);
4.4 PriorityBlockingQueue
PriorityBlockingQueue是基于优先级的阻塞队列,优先级的判断通过构造函数传入的Compator对象来决定,其特点如下:
- 不会阻塞数据生产者,只会在没有可消费的数据时阻塞数据的消费者,当生产速度快于消费速度时,否则时间一长,可能出现内存溢出;
- 内部控制线程同步的锁采用的是公平锁。
PriorityBlockingQueue常用于需要对任务执行顺序设置优先级的场景。
优先级任务类示例代码如下:
public class WorkerThread implements Runnable, Comparable<WorkerThread> {
private int id;
private int priority;
public WorkerThread(int id, int priority) {
this.id = id;
this.priority = priority;
}
@Override
public int compareTo(WorkerThread o) {
// 优先级数值越高,优先级别越低
if (this.priority < o.priority)
return -1;
else if (this.priority > o.priority)
return 1;
else
return 0;
}
@Override
public void run() {
try {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
System.out.println(format.format(new Date()) + " | The thread " + id +
" starts working, the priority is " + priority + ".");
} catch (Exception e) {
e.printStackTrace();
}
}
}
优先级任务类需要同时实现Runnable
和Comparable<T>
接口,重写Comparable<T>
接口的compareTo(T o)
方法,用于实现优先级比较规则。
测试代码如下:
public class SyncTest {
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 5, 100, TimeUnit.MILLISECONDS,
new PriorityBlockingQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
for (int i = 0; i < 5; i++) {
try {
executor.execute(new WorkerThread(i, 5 - i));
} catch (RejectedExecutionException re) {
System.out.println(format.format(new Date()) + " | The thread " + i + " was rejected.");
}
}
executor.shutdown();
}
}
在上述代码中,共启动了5个线程并加入线程池,优先级依次降低,所以这5个线程会倒序执行。保证所有任务进入优先级队列的方法有2种:
- 设置核心线程数为0;
- 预启动所有核心线程。
输出结果如下:
4.5 SynchronousQueue
SynchronousQueue是一种无缓冲的阻塞队列,具有以下特点:
- 是线程安全的阻塞队列;
- 队列的每个插入操作必须等待另一个线程的移除操作,反之亦然;
- 同步队列内部容量为0,没有任何元素,是一个空集合,不允许使用null元素;
- 公平排序策略是指调用put或take操作的线程之间;
- 不能进行peek操作,因为仅在试图要获取或移除一个元素时,该元素才存在;
- 队列的头是尝试添加到队列中的首个已排队线程元素,如果没有已排队线程,则不添加元素,并且头为 null。
SynchronousQueue中的方法具有以下特点:
方法 | 特点 |
iterator() | 永远返回空,因为队列为空。 |
peek() | 永远返回null。 |
put() | 向queue放入一个element后,就一直wait直到有其他thread取走这个element。 |
offer() | 向queue放入一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功,否则返回false。 |
offer(long timeout, TimeUnit unit) | 往queue里放一个element并等待指定时间后才返回,返回逻辑同offer()方法。 |
take() | 取出并remove掉queue里的element,获取不到会一直等待。 |
poll() | 取出并remove掉queue里的element,只有当另外一个线程正在往queue里offer数据或put数据时,该方法才会获取到数据,否则立即返回null。 |
poll(long timeout, TimeUnit unit) | 等待指定的时间后,取出并remove掉queue里的element,其实就是等待其他thread向队列放入数据。 |
isEmpty() | 永远返回true。 |
remainingCapacity() | 永远返回0。 |
remove() | 永远返回false。 |
removeAll() | 永远返回false。 |
SynchronousQueue非常适合用于传递性设计,例如一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,就必须与该对象同步。
当需要比较大的工作队列容量,而又想避免无界工作队列可能导致的内存溢出问题时,可以考虑使用SynchronousQueue。因为SynchronousQueue在实现上并不使用缓存空间,其目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理,否则新建一个线程来处理任务。
SynchronousQueue默认使用非公平排序策略,若要使用公平排序,可使用如下构造方式:
SynchronousQueue queue = new SynchronousQueue<>(true);
此时队列可保证线程以FIFO的顺序进行访问,虽然会降低吞吐量,但可减小可变性并避免出现饥饿问题。
5 拒绝策略
当线程池已满时(线程数 >= 最大线程数+阻塞队列容量),会触发线程池的拒绝策略,后续加入的线程将被拒绝。ThreadPoolExecutor提供了四个拒绝策略,分别是CallerRunsPolicy、AbortPolicy、DiscardPolicy和DiscardOldestPolicy。
5.1 AbortPolicy
AbortPolicy是ThreadPoolExecutor的默认拒绝策略,线程数量溢出时,直接抛出RejectedExecutionException异常,不再执行此任务。
简单应用示例中使用的就是AbortPolicy拒绝策略。
5.2 CallerRunsPolicy
CallerRunsPolicy在任务被拒绝添加后,会调用当前线程池的所在的线程(主线程)去执行被拒绝的任务,等于会阻塞主线程,直到主线程执行完此次任务,可用来减慢提交新任务的速度。
任务类代码同简单应用示例,测试代码如下:
public class SyncTest {
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 100, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
for (int i = 0; i < 10; i++) {
try {
executor.execute(new WorkerThread(i));
} catch (RejectedExecutionException re) {
System.out.println(format.format(new Date()) + " | The thread " + i + " was rejected.");
}
}
executor.shutdown();
}
}
在上述示例代码中,线程池大小为5,阻塞队列大小为3,共10个任务将加入线程池。输出结果如下:
由输出结果可知,由于使用的是CallerRunsPolicy策略,所以当加入5个任务开始执行后,仍可加入第6个任务,此任务由主线程负责执行,此时主线程被阻塞,不再添加新的任务,知道主线程执行任务完毕,才开始继续添加剩余的4个任务。
5.3 DiscardPolicy
DiscardPolicy会让被线程池拒绝的任务直接被抛弃,不会抛出异常,也不会执行,对于被拒绝的任务没有任何感知。
5.4 DiscardOldestPolicy
DiscardOldestPolicy策略下,当一个新任务被拒绝添加时,会抛弃任务队列中最旧的任务(最先加入队列的任务),再把这个新任务添加到队列中。
任务类代码同简单应用示例,测试代码如下:
public class SyncTest {
/**
* 创建线程池
*/
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 100, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.sss");
for (int i = 0; i < 9; i++) {
try {
// 向线程池中添加任务
executor.execute(new WorkerThread(i));
System.out.println(format.format(new Date()) +
" | The thread " + i + " joined the thread pool.");
Thread.sleep(100);
} catch (RejectedExecutionException re) {
System.out.println(format.format(new Date()) + " | The thread " + i + " was rejected.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executor.shutdown();
}
}
在上述示例代码中,线程池大小为3,阻塞队列大小为3,共9个任务将加入线程池。输出结果如下:
由输出结果可知,由于使用DiscardOldestPolicy策略,先加入队列的线程2、3、4被依次抛出,转而执行后加入的线程6、7、8。
5.5 自定义拒绝策略
我们也可以选择使用自定义的拒绝策略,自定义拒绝策略类需实现RejectedExecutionHandler
接口。
自定义拒绝策略类示例代码如下:
public class MyRejectedPolicy implements RejectedExecutionHandler {
public static ConcurrentHashMap<Integer, Runnable> taskCacheMap = new ConcurrentHashMap<>();
public static Integer count = 0;
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
taskCacheMap.put(count, r);
count++;
}
}
在上述代码的自定义拒绝策略中,被拒绝的线程将被加入一个ConcurrentHashMap,等待取出执行。
构造线程池对象时,将拒绝策略对象换成自定义拒绝策略类的对象即可,如下:
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 100, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new MyRejectedPolicy());
6 线程工厂
创建线程池时,默认使用线程创建工厂为Executors.defaultThreadFactory()
,全部线程位于同一个ThreadGroup中,并且具有相同的NORM_PRIORITY优先级和非守护进程状态。
如果想要实现额外的功能,也可以使用自定义的线程创建工厂,需要实现ThreadFactory
接口。通过提供不同的ThreadFactory,可以更改线程的名称、线程组、优先级和守护进程状态等。
自定义线程创建工厂类示例如下:
public class MyThreadFactory implements ThreadFactory {
private final AtomicInteger mThreadNum = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "thread-" + mThreadNum.getAndIncrement());
System.out.println("The " + t.getName() + " has been created.");
return t;
}
}
在上述代码的自定义线程创建工厂中,创建的线程增加了自定义的线程名称,并打印创建日志。
构造线程池对象时,将线程工厂对象换成自定义线程工厂类的对象即可,如下:
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 100, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(3),
new MyThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
每个线程运行时,可通过Thread.currentThread()
获取当前线程信息,再通过getName()
函数获取当前线程名称,示例如下:
System.out.println(format.format(new Date()) +
" | The " + Thread.currentThread().getName() + " starts working.");
输出结果如下:
7 钩子
ThreadPoolExecutor提供了每个任务执行前后的钩子方法,可通过重写beforeExecute(Thread,Runnable)
和afterExecute(Runnable,Throwable)
方法来操纵执行环境,例如:重新初始化ThreadLocals、收集统计信息、记录日志等。另外,Executor完全终止后会调用terminated()
方法,可以重写此方法来执行任殊处理。如果hook或回调方法抛出异常,内部的任务线程将会失败并结束运行。
8 维护队列
getQueue()
方法可以访问线程池的阻塞队列,一般用于监控和调试。当大量队列中的任务被取消时,remove()
和purge()
方法可用于回收内存空间。