目录
一 什么是线程池
线程池就是线程集合的池子,是一种将任务添加到队列,然后创建线程后自动启动这些任务的流程。
二 为什么需要线程池
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
使用线程池有以下几个目的:
- 线程是稀缺资源,不能频繁的创建。
- 线程的创建与执行完全分开,实现解偶,便于维护。
- 线程能够得到复用。
三 创建线程池的四种方式
- Executors.newCachedThreadPool():无限线程池。
- Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。
- Executors.newSingleThreadExecutor():创建单个线程的线程池。
- Executors.newScheduledThreadExecutor():创建一个定长线程池,支持定时及周期性任务执行。
四种方式,通过查看源码都是通过:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
创建的。
这几个核心参数的作用:
- corePoolSize:核心线程数目。
- maximumPoolSize :最大线程数。
- keepAliveTime 和 unit :则是非核心线程空闲后的存活时间。
- workQueue :存放任务的阻塞队列。
- handler:阻塞队列和最大线程池都满了之后的饱和策略。
上述4种线程池说明:
3.1 FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
3.2 CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
四 execute() 方法执行过程
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
1. 如果正在运行的线程数量小于 corePoolSize,马上创建线程运行这个任务;
2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入阻塞队列。
3. 将任务放入阻塞队列时,如果阻塞队列没有满,则将当前任务存储在队列中;
4. 如果阻塞队列满了,而且正在运行的线程数量小于 maximumPoolSize,创建线程运行这个任务。
5. 如果正在运行的线程数量大于或等于 maximumPoolSize,那么会执行饱和策略。
五 饱和策略说明
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
拒绝策略是一个接口,其设计如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,其特点如下:
- ThreadPoolExecutor.AbortPolicy : 丢弃任务并抛出 RejectedExecutionException 异常。这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子系统在不能承载更大的并发时候,能够及时通过异常发现。
- ThreadPoolExecutor.DiscardPolicy : 丢弃任务,但是不抛出异常。使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务使用此策略。
- ThreadPoolExecutor.DiscardOldestPolicy : 丢弃队列最前面的任务,然后重新提交被拒绝的任务。是否要采用此拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
- ThreadPoolExecutor.CallerRunsPolicy : 由调用线程处理该任务,这种情况是需要让所有任务都执行完毕,那么就适合大量计算的任务类型去执行,多线程仅仅是增大吞吐量的手段,最终必须要让每个任务都执行完毕。
六 核心线程数设置
一般多线程执行的任务类型可以分为CPU密集型和I/O密集型,根据不同的任务类型,我们计算线程数的方法也不一样。
CPU密集型:消耗的主要是CPU资源,建议核心线程数设置为N(CPU核心数)+1。为什么是 N+ 1 ?因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。
I/O密集型:大部分的时间来处理I/O交互,建议核心线程数设置为CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]。