为什么要使用线程池
在日常开发中为了提高代码运行效率,或多或少会用线程去执行异步任务,线程的创建和销毁是需要占用一定资源的。
首先我们看一下一个线程的创建步骤:
- 为线程堆栈分配和初始化大量内存块
- 需要进行系统调用,以便在主机OS中创建/注册本机线程
- 描述符需要创建、初始化并添加到JVM内部数据结构中
而池化技术的出现是为了重复利用已存在的线程,避免了频繁的创建和销毁。
线程池的初始化及参数
注意:线程池必须手动通过 ThreadPoolExecutor 的构造函数来声明,避免使用Executors 类的 newFixedThreadPool 和 newCachedThreadPool ,因为可能会有 OOM 的风险。
看下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 核心线程数
核心线程:要保留在池中的线程数,即使它们处于空闲状态,线程池创建的时候是空的,随着任务的调用,逐步增加到核心线程数的大小。当任务进来时,如果有核心线程空闲,则优先使用核心线程。
如何设计核心线程池的数量:
- 如果是CPU密集型应用,则线程池大小设置为N+1 (N为CPU总核数)
- 如果是IO密集型应用,则线程池大小设置为2N+1 (N为CPU总核数)
- 线程等待时间(IO)所占比例越高,需要越多线程。
- 线程CPU时间所占比例越高,需要越少线程。
1. 题外话:cpu密集型和IO密集型是什么
是任务、方法的类型
1. cpu密集型(计算密集型、cpu高了)
处理运算时间比较长,系统运行的大部分状况是CPU Loading 100%,不太需要访问I/O设备
1. 例如
计算圆周率、对视频进行高清解码
2. 要注意什么
尽量避免CPU的切换,任务同时进行的数量 = CPU的核心数
1. IO密集型
IO的速度远远低于CPU和内存的速度,cpu性能好,处理运算时间比较短,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作
1. 例如
Web应用
2. 要注意什么
可以充分利用CPU的资源,但不能开启任务数量太多,一般情况:任务同时进行的数量 = 2*CPU的核心数
maximumPoolSize 最大线程数
池中允许的最大线程数
keepAliveTime 非核心线程的活跃时间
当线程数大于核心数时,非核心线程如果过了keepAliveTime长时间还没有执行新的任务,则销毁线程。
unit 单位
是keepAliveTime的时间单位
workQueue 队列
用于在执行任务之前保存任务的队列。该队列将仅保存由 {@code execute} 方法提交的 {@code Runnable} 任务。
workQueue的类型为BlockingQueue,通常可以取下面三种类型:
队列的种类:
- ArrayBlockingQueue 有界任务队列
基于数组的先进先出队列,此队列创建时必须指定大小;FIFO
- LinkedBlockingQueue 无界任务队列
基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;FIFO
- SynchronousQueue 同步队列
一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene
- PriorityBlockingQueue 优先级队列
一个支持优先级的无界阻塞队列,可以通过其构造方法,自定义排序的规则,默认按照首字母从小到大排序,生产的时候随便,消费的时候会按照优先级消费
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
threadFactory 线程工厂
工厂类,创建新线程时使用的工厂
handler 饱和策略
由于达到线程边界和队列容量而阻塞执行时要使用的处理程序,默认是AbordPolicy,表示无法处理新任务,并抛出 RejectedExecutionException 异常
1. AbortPolicy(拒绝抛出异常)
/**
* A handler for rejected tasks that throws a
* {@code RejectedExecutionException}.
*/
public static class AbortPolicy implements RejectedExecutionHandler {
}
丢弃任务并抛出RejectedExecutionException异常,默认策略
如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
2. CallerRunsPolicy(让外面的线程去处理)
/**
* A handler for rejected tasks that runs the rejected task
* directly in the calling thread of the {@code execute} method,
* unless the executor has been shut down, in which case the task
* is discarded.
*/
public static class CallerRunsPolicy implements RejectedExecutionHandler {
}
由调用线程(执行execute的线程)去处理该任务,如果调用线程关闭,则直接抛弃该任务
3. DiscardOldestPolicy(把最老的丢掉、喜新厌旧)
/**
* A handler for rejected tasks that discards the oldest unhandled
* request and then retries {@code execute}, unless the executor
* is shut down, in which case the task is discarded.
*/
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
}
丢弃队列最老的未执行的任务,然后重新提交被拒绝的这个任务(喜新厌旧的策略)
是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
4. DiscardPolicy(偷偷的抛弃,不抛异常)
/**
* A handler for rejected tasks that silently discards the
* rejected task.
*/
public static class DiscardPolicy implements RejectedExecutionHandler {
}
直接抛弃该任务,不会抛出异常。使用该策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
1.例如
阅读量、点击量这些高频的但不需要很精确操作