线程池的介绍
不用线程池存在的问题
- 反复创建销毁线程开销大
- 过多的线程占用大量内存
线程池的使用,就类似于计划经济,控制资源总量,复用线程。有如下3点好处。
- 加快响应速度。消除了线程创建带来的延时。
- 合理的利用CPU和内存。控制线程的数量,既不会因为线程太多导致内存溢出,也不会因为太少导致CPU资源的浪费。
- 便于统一管理线程。例如数据统计。
线程的使用场景举例:
- 服务器接收大量请求,使用线程池可以大大减少线程创建和销毁次数,提高服务器工作效率。
- 开发中使用多个线程时,可以考虑采用线程池。
线程池结构
ThreadPoolExecutor继承AbstractExecutorService(实现ExecutorService),4个类/接口都可以视作线程池,并向上转型(多态)。ThreadPoolExecutor中有5个嵌套类,其中4个为RejectedExecutionHandler拒绝策略接口的实现类,1个是Worker类,用于维护正在运行任务的线程的中断控制状态,以及其他的次要信息。
Executors是线程池工具类,可以通过静态工厂方法快捷实现线程池,例如Executors.newFixedThreadPool(10).
举个面包店的例子
假设有一所面包店,面包店平时有5位面包师,每天随着订单增加,5位师傅逐渐加入到工作中去。不能及时处理的订单,会挂在墙上的订单栏上,等待面包师顺序处理。
订单过多时,5个面包师忙不过来就招临时面包师,最多5名。根据任务多少会调整临时面包师的数量,太长时间没事干的临时面包师会被辞退掉。
店铺关闭或者订单在面包师达到10名后,便不再接新单。
线程池构造器
ThreadPoolExecutor只有四个构造器,最多有7个参数。其中前5个是必要参数,线程工厂和拒绝策略可选。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
线程添加策略
- 如果线程数小于corePoolSize,即使其他工作线程处于空闲状态,仍会创建新线程运行新任务。【疑问:是一下全建出来,还是一个一个创建】
- 如果线程数大于corePoolSize但是少于maxPoolSize,则将任务放入到阻塞队列中。
- 如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程运行任务。
- 如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务。
增减线程的特点
- 如果设置相同的corePoolSize和maxThreadPoolSize,相当于创建固定大小的线程池。此时,工作队列通常选用无界队列,超时时间为0L。
- 线程池希望保持较少的线程数,只在负载变得很大的时候才增加新的线程。
- 通过设置maxPoolSize为Integer.MAX_VALUE,可以允许线程池容纳任意容量的并发任务。
- 只有在队列填满时,才会创建多于corePoolSize的新线程。如果采用无界队列,例如LinkedBlockingQueue,线程数不会超过corePoolSize。
线程池的拒绝策略
拒绝时机–参见execute方法中两个调用位置
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
- 当Executor关闭时,提交新任务会被拒绝;
- 当Executor对最大线程数和工作队列容量使用有限边界,并且已经饱和时。
拒绝策略
- AbortPolicy:直接抛出异常,提示没有提交成功
- DiscardPolicy:直接丢弃新任务,不抛出异常。
- DiscardOldestPolicy:丢弃最老的任务
- CallerRunsPolicy:让提交任务的线程执行。好处是避免了业务损失;提交速度降低(主线程一直提交任务,线程池和工作队列满后,主线程开始执行提交的任务,相当于给了线程池一个缓冲的时间)。
源码分析
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
拒绝策略接口只有一个方法,目的很明确。
4种拒绝策略实现RejectedExecutionHandler接口,代码实现也很简单清楚,此处只介绍DiscardOldestPolicy和CallerRunsPolicy两种策略。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() {
}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 1. 如果线程池是RUNNING状态,并且线程达到饱和,则丢弃最老的任务,重新执行execute方法
// 2. 如果线程池非RUNNING状态,则直接丢弃任务。
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() {
}
// 1. 如果线程池是RUNNING状态,并且线程达到饱和,则使用调用者线程执行任务
// 2. 如果线程池非RUNNING状态,则直接丢弃任务。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
我们知道直接调用runnable的run()方法是同步调用,调用者线程也就是调用当前线程池execute方法的那个线程。
工作队列
工作队列通用策略有3种。
- 直接提交队列:默认为SynchronousQueue,直接将任务提交给线程,不保持他们。如果不存在空闲的线程,则试图将任务加入队列将失败,会新创建一个线程。通常要求无界
maximumPoolSizes 。 - 无界队列:例如LinkedBlockingQueue,用于突发请求。
- 有界队列:例如ArrayBlockingQueue,有利于防止资源耗尽,较难调整队列大小和最大池大小的值。
线程工厂
public interface ThreadFactory {
Thread newThread(Runnable r);
}
ThreadFactory只有一个方法,是一个函数式接口。
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);
// 线程的安全级别是5
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
常见的线程池
Executors快捷生成的线程池参数总结。
默认的线程工厂ThreadFactory是Executors.defaultThreadFactory(),拒绝策略是AbortPolicy。
- SingleThreadPool:内部和FixedThreadPool基本一致,只是线程数不同。
- CachedThreadPool:创建可缓存线程池。是无界线程池,具有自动回收多余线程的功能。采用的是同步移交队列,任务直接给到空闲线程或者创建新线程执行。如果线程空闲(没有任务执行)60秒,回收线程。
- ScheduledThreadPool:支持定时及周期性任务执行的线程池。
- WorkStealingPool是JDK8中新增的一种线程池,是新的线程池类ForkJoinPool的扩展,能够合理地使用CPU对任务做并行操作,适合耗时的场景,例如递归、分而治之,并且不加锁的场景。
线程池的注意点
避免任务堆积,避免线程数过度增加,排查线程泄漏。线程多时,有可能存在线程泄漏,线程执行完毕但是没被回收,可能是任务逻辑问题。