一、引言
1.1 为什么使用线程池
-
在Java中,如果每个请求到达就创建一个新的线程,开销会非常大。
-
在实际使用中,服务器在创建和销毁线程上花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多;如果一个JVM里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。
-
为了防止资源不足,服务器应用程序需要采取一些方法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
概括起来就是: 减低资源消耗,提供响应速度,提供线程的可管理性
降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
1.2 什么是阻塞队列
队列:一种线性表,它的特性是先进先出,插入在一端,删除在另一端
队列又分为阻塞队列BlockingQueue和非阻塞队列ConcurrentLinkedQueue
它们两者的区别:
非阻塞队列: 如果存放超出队列容量,则队列信息丢失;获取队列元素时,若队列为空,则返回null
阻塞 队列: 如果存放超出队列容量,则进行等待(等待时间为队列设置的消费者等待超时时间);获取队列元素时,若队列为空,则进行等待。
阻塞队列在操作中若规定了超时时间,但是操作不需要等待时,就不用等待;若在规定的超时时间内没有完成操作,则放弃操作。若不规定超时时间,就可以认为是非阻塞队列。
二、 线程池
2.1 线程池组成
一个比较简单的线程池至少应包含线程池管理器、工作线程、任务列队、任务接口等部分。
-
线程池管理器 (ThreadPool) 的作用是创建、销毁并管理线程池,将工作线程放入线程池中;
-
工作线程 (PoolWorker) 是线程池中的线程,可以循环执行任务,在没有任务时处于等待状态;
-
任务列队 (taskQueue) 的作用是提供一种缓冲机制,将没有处理的任务放在任务列队中;
-
任务接口 (task) 是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执行状态等,工作线程通过该接口调度任务的执行。
2.2 线程池的使用
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程数。默认情况下,核心线程会一直存活(当allowCoreThreadTimeout设置为true时会被回收)
- maximumPoolSize: 线程池所能容纳的最大线程数。
- keepAliveTime:线程闲置超时时长,如果超过该时长,非核心线程会被回收。如果allowCoreThreadTimeout设置为true时,核心线程也会超时回收
- unit:指定keepAliveTime参数的时间单位
- workQueue:等待队列,任务可以存储在此队列中等待被执行,执行的是FIFO原则。其采用阻塞队列实现
- threadFactory(可选参数):创建线程的线程工厂
- handler(可选参数):拒绝策略,当达到最大线程数时需要执行的饱和策略(四种拒绝策略如下)
- AbortPolicy: 不执行新任务,直接抛出异常,提示线程池已满
- DisCardPolicy: 不执行新任务,也不抛出异常
- DisCardOldSetPolicy: 将等待队列中的第一个替换为当前新进来的任务执行
- CallerRunsPolicy: 直接调用execute来执行当前任务
三、任务执行流程
3.1 添加执行任务
- submit() 该方法返回一个Future对象,可执行带返回值的线程;或者执行想随时可以取消的线程。Future对象的get()方法获取返回值。Future对象的cancel(true/false)取消任务,未开始或已完成返回false,参数表示是否中断执行中的线程
- execute() 没有返回值。
3.2 任务提交过程 (工作原理)
- 首先判断核心线程数(corePoolSize),如果核心线程数未满,则创建新的线程来执行添加的任务;
- 如果核心线程数(corePoolSize)满了,进而判断阻塞队列(workQueue),如果阻塞队列未满,则将任务加入到阻塞队列中等待执行;
- 如果核心线程数(corePoolSize)满了,且阻塞队列(workQueue)也满了,则判断线程池数量(maximumPoolSize),如果线程池未满,则创建新的线程执行添加的任务;
- 如果核心线程数(corePoolSize)满了,阻塞队列(workQueue)满了,且线程池数量(maximumPoolSize)也满了,则通过handler所指定的策略来处理此任务
总结即:处理任务判断的优先级为 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务
注:
- 当workQueue使用的是无界限队列时,maximumPoolSize参数就变的无意义了,比如new LinkedBlockingQueue(),或者new ArrayBlockingQueue(Integer.MAX_VALUE);
- 使用SynchronousQueue队列时由于该队列没有容量的特性,所以不会对任务进行排队,如果线程池中没有空闲线程,会立即创建一个新线程来接收这个任务。maximumPoolSize要设置大一点。
- 核心线程和最大线程数量相等时keepAliveTime无作用.
3.3 线程池关闭
- shutdown() 不接收新任务,会处理已添加任务
- shutdownNow() 不接受新任务,不处理已添加任务,中断正在处理的任务
3.4 常用队列介绍
- ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列.
- SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。
- LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。
四、功能线程池
1. 定长线程池 newFixedThreadPool
源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
-
特点: 只有核心线程,线程数固定,执行完立即回收,任务队列为链表结构的有界队列
-
应用场景: 控制线程最大并发数
使用实例
// 1. 创建定长线程池对象,设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 2. 创建好Runnable类线程对象,执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("开始执行任务-NewFixedThreadPool");
}
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
2. 定时线程池 newScheduledThreadPool
源码
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
- 特点:核心线程数固定,非核心线程数量不限,执行完闲置10ms后回收,任务队列为延时阻塞队列
- 应用场景: 执行定时或周期性的任务
使用实例
// 1. 创建 定时线程池对象,设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象,执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("开始执行任务-newScheduledThreadPool");
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
3. 可缓存线程池 newCacheThreadPool
源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
- 特点:无核心线程,非核心线程数不限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列
- 应用场景:执行大量,耗时少的任务
4. 单线程化线程池 newSingleThreadExecutor
源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
- 特点: 只有一个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列
- 不适合并发但可能引起IO阻塞,及影响UI线程响应的操作,如数据库操作文件操作等
5. 总结
以上四个功能线程的方法虽然方便但是不建议使用,建议直接使用ThreadPoolExecutor的方式创建,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险
FixedThreadPool和SingleThreadExecutor:主要问题是堆积的请求处理队列均采用LinkedBlockingQueue,可能会耗费非常大的内存,甚至OOM
CachedThreadPool和ScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
五、线程池配置
在进行线程池配置时可以根据任务的性质来划分线程池类型: 计算密集型(CPU),IO密集型,混合型
计算密集型:一般配置CPU处理器个数+/-个线程
IO密集型:指系统大部分时间都在跟I/O交互,这种情况下可以多配置些线程 读取文件,数据库连接,网络通信;线程数适当大一点,机器的CPU核数*2
混合型: 尽量拆分,IO密集型>>计算密集型
队列选择: 尽可能选择有界,无界队列可能会导致内存溢出(OOM)