ThreadPoolExecutor自定义线程池示例
核心内容导读
- 简单讲解为什么阿里不推荐使用Executors创建线程池
- 自定义线程池的几大基本属性含义
- 并发不高、任务执行时间长的业务如何创建自定义线程池(实例)
一、为什么阿里规范强制使用ThreadPoolExecutor?
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool :
允许的请求队列的长度可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool :
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
我们可以通过自定义线程池去规避这些问题。
1.SingleThreadExecutor源码分析
源码如下:
/**
* Creates an Executor that uses a single worker thread operating
* off an unbounded queue. (Note however that if this single
* thread terminates due to a failure during execution prior to
* shutdown, a new one will take its place if needed to execute
* subsequent tasks.) Tasks are guaranteed to execute
* sequentially, and no more than one task will be active at any
* given time. Unlike the otherwise equivalent
* {@code newFixedThreadPool(1)} the returned executor is
* guaranteed not to be reconfigurable to use additional threads.
*
* @return the newly created single-threaded Executor
*/
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 根据源码可以看出,SingleThreadExecutor的核心线程数=最大线程数=1,非核心线程空闲存活时间=0,存放任务的队列是默认Integer.MAX_VALUE(无界)的LinkedBlockingQueue,它的拒绝策略是默认的 AbortPolicy (丢弃任务并抛出RejectedExecutionException)。
- 它只会用唯一的工作线程来执行任务,即单线程化的线程池,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- 但由于它的队列无界,新的任务会不断放入队列中,队列是一种数据结构,会占用内存空间,所以SingleThreadExecutor会造成工作任务过多导致内存溢出(OOM)的情况。
2.CachedThreadPool源码分析
源码如下:
/**
* Creates a thread pool that creates new threads as needed, but
* will reuse previously constructed threads when they are
* available. These pools will typically improve the performance
* of programs that execute many short-lived asynchronous tasks.
* Calls to {@code execute} will reuse previously constructed
* threads if available. If no existing thread is available, a new
* thread will be created and added to the pool. Threads that have
* not been used for sixty seconds are terminated and removed from
* the cache. Thus, a pool that remains idle for long enough will
* not consume any resources. Note that pools with similar
* properties but different details (for example, timeout parameters)
* may be created using {@link ThreadPoolExecutor} constructors.
*
* @return the newly created thread pool
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 根据源码可以看出,CachedThreadPool的核心线程数=0,最大线程数是无界的,非核心线程空闲存活时间=60s,存放任务的队列是不存储任务的SynchronousQueue,它的拒绝策略是默认的 AbortPolicy (丢弃任务并抛出RejectedExecutionException)。
- 由于核心线程数=0,且为SynchronousQueue,故CachedThreadPool线程池 无界的非核心线程可灵活回收空闲线程,若无可回收,则无限创建线程,直至OOM。
- 但由于它的非核心线程数无界,过多的新任务会导致不断创建新的线程,所以CachedThreadPool会造成CPU一直在运行不同线程,导致CPU100%。
3. FixedThreadPool源码分析
源码如下:
/**
* Creates a thread pool that reuses a fixed number of threads
* operating off a shared unbounded queue. At any point, at most
* {@code nThreads} threads will be active processing tasks.
* If additional tasks are submitted when all threads are active,
* they will wait in the queue until a thread is available.
* If any thread terminates due to a failure during execution
* prior to shutdown, a new one will take its place if needed to
* execute subsequent tasks. The threads in the pool will exist
* until it is explicitly {@link ExecutorService#shutdown shutdown}.
*
* @param nThreads the number of threads in the pool
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code nThreads <= 0}
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 根据源码可以看出,FixedThreadPool的核心线程数和最大线程数都是可自定义,非核心线程空闲存活时间0s,存放任务的队列是默认Integer.MAX_VALUE(无界)的LinkedBlockingQueue,它的拒绝策略是默认的 AbortPolicy (丢弃任务并抛出RejectedExecutionException)。
- 固定了N个线程虽然可以防止无限创建新的线程,但提交给线程池的任务队列是不限制大小的,即它的队列无界,新的任务会不断放入队列中,队列是一种数据结构,会占用内存空间,所以FixedThreadPool会造成工作任务过多导致内存溢出(OOM)的情况。
一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结
4. ScheduledThreadPoolExecutor源码分析
源码如下:
/**
* Creates a thread pool that can schedule commands to run after a
* given delay, or to execute periodically.
* @param corePoolSize the number of threads to keep in the pool,
* even if they are idle
* @return a newly created scheduled thread pool
* @throws IllegalArgumentException if {@code corePoolSize < 0}
*/
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
/**
* Creates a new {@code ScheduledThreadPoolExecutor} with the
* given core pool size.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @throws IllegalArgumentException if {@code corePoolSize < 0}
*/
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
// 数组的初始容量为16 设置为16的原因跟hashmap中数组容量为16的原因一样
private static final int INITIAL_CAPACITY = 16;
// 用于记录RunableScheduledFuture任务的数组
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
// 当前队列中任务数,即队列深度
private int size = 0;
// leader线程用于等待队列头部任务,
private Thread leader = null;
// 当线程成为leader时,通知其他线程等待
private final Condition available = lock.newCondition();
- ScheduledThreadPoolExecutor使用场景:定时任务,要求时间误差小,稳定。
- 正常情况下,定时器我们都是用Timer和TimerTask这两个类就能完成定时任务,并且设置延长时间和循环时间间隔。
ScheduledThreadPoolExecutor也能完成Timer一样的定时任务,并且时间间隔更加准确。
定时任务ScheduledThreadPoolExecutor的使用详解 - 根据源码可以看出,ScheduledThreadPoolExecutor的核心线程数可自定义,最大线程数是Integer.MAX_VALUE,非核心线程存活时间0s,存放任务的队列是定制的优先级队列, 只能用来存储RunnableScheduledFutures任务的DelayedWorkQueue,有OOM风险。它的拒绝策略是默认的 AbortPolicy (丢弃任务并抛出RejectedExecutionException)。
- scheduleAtFixedRate的jdk文档中:如果在任务的执行中遇到异常,后续执行被取消。
1.ScheduledThreadPoolExecutor遇到的坑
2.理解优先队列DelayedWorkQueue
二、自定义线程池的几大基本属性含义
1.ThreadPoolExecutor源码分析
源码如下:
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize:核心线程个数,如果不设置 allowCoreThreadTimeOut 会保持一直在线程池中,即使是空闲状态。
- maximumPoolSize :线程池中最大线程数。
- keepAliveTime:当前所需运行线程数 > 核心线程数时,非核心线程空闲状态等待被分配任务的最大时间,超过该时间后非核心线程将被回收。
- unit:keepAliveTime 的时间单位
- workQueue:当线程池所有线程都在工作时,新的任务会被放到该队列中等待被分配。这个队列将只保存实现 Runnable 接口,并通过 execute 方法提交的任务。
- threadFactory:执行器创建新线程时使用的工厂,ThreadPoolExecutor默认的工厂为 DefaultThreadFactory。
- handler:当工作任务数 > 最大线程数+队列数 时,新的任务的拒绝策略,默认为AbortPolicy。
2.threadFactory选型介绍
- DefaultThreadFactory代码如下:
/**
* 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;
}
}
- 源码图示:
3.handler(拒绝策略)选型介绍
- AbortPolicy 中止策略:丢弃任务并抛出RejectedExecutionException。
- CallerRunsPolicy 调用者运行:只要线程池未关闭,用调用者所在的线程来执行当前新任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
- DiscardOldestPolicy 抛弃旧任务策略:丢弃队列中最旧的一个任务(即将被执行的一个任务),并执行最新的任务,如果执行器被关闭则任务被丢弃。–喜新弃旧型
- DiscardPolicy 抛弃策略:丢弃任务,不抛出异常,不做任何操作。
4.workQueue选型介绍
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;有界存储多余任务的队列。有效防止OOM
一个 ArrayBlockingQueue 不当使用,导致公司损失几百万! - LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;可以人为指定队列大小。默认接近无界,可以设置为有界的,可存储多余任务的队列。无界时会造成OOM。
- SynchronousQueue:SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。拥有公平(FIFO)和非公平(LIFO)策略,非公平策略会导致一些数据永远无法被消费的情况。如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。只有在使用无界线程池或者有饱和策略时才建议使用该队列。使用SynchronousQueue要避免线程拒绝执行操作。不会OOM,但可能造成CPU长时间被占用,CPU100%。
- PriorityBlockingQueue:通过二叉小顶堆实现,可以用一棵完全二叉树表示。有优先级的无界阻塞队列,默认情况下元素采用自然顺序升序排列,也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。但需要注意的是不能保证同优先级元素的顺序。
PriorityBlockingQueue深度解析 - DelayedWorkQueue: DelayedWorkQueue核心数据结构是二叉最小堆的优先队列,队列满时会自动扩容,所以offer操作永远不会阻塞,maximumPoolSize也就用不上了,所以线程池中永远会保持至多有corePoolSize个工作线程正在运行
Queue常用类解析之BlockingQueue(一):PriorityBlockingQueue、DelayQueue和DelayedWorkQueue - BlockingQueue核心方法描述:
三、并发不高、任务执行时间长,不接收返回值的业务创建自定义线程池实例
-
业务场景:
由于接口处理时间较长,所以将耗时部分设计为异步执行,主线程先返回成功请求状态码,子线程继续执行,且主线程无需等待子线程返回结果。 -
实现逻辑:
//创建线程池,异步执行SynchronousQueue选型防止OOM,拒绝策略用默认AbortPolicy,不丢弃任务,失败时抛异常,最大10个防止线程过多
ExecutorService threadPool=new ThreadPoolExecutor(0, 10,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
threadPool.execute(() -> {
//子线程异步执行的代码块,无返回值
});
//关闭线程池
threadPool.shutdown();
自定义线程池属性:异步执行SynchronousQueue选型防止OOM,拒绝策略用默认AbortPolicy,不丢弃任务,失败时抛异常,最大10个防止线程过多。