15. 线程池
15.1 为什么要使用线程池?
-
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
-
提高响应速度:任务到达时,任务可以不需要等到线程创建就能立即执行
-
提高线程的可管理性
15.2 线程池的体系结构:
java.util.concurrent.Executor : 负责线程的使用与调度的根接口
- ExecutorService 子接口: 线程池的主要接口
- ThreadPoolExecutor 线程池的实现类
- ScheduledExecutorService 子接口:负责线程的调度
- ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor,实现 ScheduledExecutorService
15.3 创建线程池的方法
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS, new ArrayBlockingQueue(10));
/***********************************************************************/
public ThreadPoolExecutor(int corePoolSize, //核心池大小大小
int maximumPoolSize, //最大容量
long keepAliveTime, //线程数大于corePoolSize后,空闲存活时间
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
15.3.1 线程池的拒绝策略
等待队列也已经满了,再也塞不下新任务了。同时线程池中的max线程数也达到了,无法继续为新任务服务。这时候我们就需要拒绝策略机制合理的解决这个问题。
-
AbortPolicy 默认 抛出RejectedExecutionException异常阻止系统正常运行
-
CallerRunsPolicy 该策略既不会抛出任务,也不会抛出异常,而是将某些任务交由调用者完成。
-
DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务。
-
DiscardPolicy 直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
15.3.2 线程池的工作队列
-
ArrayBlockingQueue (有界队列)
-
LinkedBlockingQueue (无界队列)可以指定对立的大小,也可以不指定,默认类似一个无限大小的容量(Integer.MAX_VALUE)
-
SynchronousQueue(同步队列) 不存储元素,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态, 吞吐量通常要高于LinkedBlockingQueue
-
DelayQueue(延迟队列) 一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序
-
PriorityBlockingQueue(优先级队列)
有界队列即长度有限,满了以后ArrayBlockingQueue会插入阻塞。无界队列就是里面能放无数的东西而不会因为队列长度限制被阻塞,但是可能会出现OOM异常。
15.4 线程池的提交与关闭方法
- threadPool.execute(Runnable task) 提交无返回值的方法
- threadPool.submit(Callable task) 提交有返回值的方法,返回一个future对象
- threadPool.shutdown() 等待任务执行完关闭
- threadPool.shutdownNow() 立即关闭
15.5 线程池的底层工作原理
-
在创建线程池后,等待提交过来的任务请求。
-
调用execute()方法提交一个新任务到线程池,处理流程:
- 判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程执行任务。
- 判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。
- 判断线程池的线程是否都处于工作状态。如果没有,则创建新的工作线程来执行任务。如果满了,则交给饱和策略来处理这个任务。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
-
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
-
所以当线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
以ThreadPoolExecutor执行execute方法举例,分为4种情况:
-
如果当前运行线程数少于corePoolSize,则创建新线程来执行任务
-
如果运行的线程等于或多余corePoolSize,则将任务加入BlockingQueue
-
如果BlockingQueue已满,则创建新的线程来处理任务
-
如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用对应的策略
工作线程:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里 的任务来执行。
15.6 使用线程池的风险
- 死锁:线程池引入了另一种死锁可能,所有池程都在执行已阻塞的等待队列中另一任务的执行结果的任务,但这一任务却因为没有未被占用的线程而不能运行。
- 资源不足
- 并发错误
- 线程泄漏:当从池中一个线程执行任务后该线程却没有返回池时,会发生这种情况。
- 请求过载
15.7 创建线程池的工具类 : Executors
线程池种类 | 特点 |
---|---|
newFixedThreadPool() | 创建固定大小的线程池,核心线程数和最大线程数大小一样,keepAliveTime为0,阻塞队列是LinkedBlockingQueue,处理CPU密集型的任务。 |
newCachedThreadPool() | 核心线程数为0,最大线程数为Integer.MAX_VALUE,keepAliveTime为60s,阻塞队列是SynchronousQueue,并发执行大量短期的小任务。 |
newSingleThreadExecutor() | 创建单个线程池。核心线程数和最大线程数大小一样且都是1,keepAliveTime为0,阻塞队列是LinkedBlockingQueue,按添加顺序串行执行任务。 |
newScheduledThreadPool() | 创建固定大小的线程,最大线程数为Integer.MAX_VALU,阻塞队列是DelayedWorkQueue |
注意:
- FixedThreadPool 和 SingleThreadPool允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
- CachedThreadPool 和 ScheduledThreadPool 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。