线程池的正确使用时java高并发的基础,你曾经因为不了解线程池导致OOM吗?曾经因为不了解队列缓存导致任务丢失吗?曾经因为错误填写核心线程数导致逾期的执行效果不能达到吗?让我们来一起来快速的了解线程池的使用吧!
预定义线程池的种类
java.util.concurrent包下的Executors类中包含6种已经实现好的static线程池工具
- newFixedThreadPool
- init方法可传入固定线程数
- 创建可重用的且固定数量的线程的线程池,操作共享的无边界队列
- 在任何时间点,固定数量的线程都是活跃的。如果所有线程都处在执行任务状态,又提交了其他任务,那么这些任务会在队列中等待可用的线程。
- 一个线程在执行完成之前由于失败被终止的话,如果需要可以更换一个新的线程执行后续任务。
- 线程池中的线程会一直的持续下去,直到有程序明确执行了ExecutorService#shutdown方法之后。
- newWorkStealingPool
- init方法可传入并行执行级别
- 线程池中存在一定数量的线程,并且足够支持给定并行级别,并且可以使用多个队列来减少任务间互相争用线程的情况。
- 并行级别对应于主动参与活可用于执行任务处理的最大线程池数,实际线程数可能动态的增加或减少。
- 这种线程池不能保证提交任务的执行顺序。
- 另外需要注意,如果初始化是不指定并行级别的话,会默认取Runtime.getRuntime().availableProcessors(),在不同主机配置不同插槽不同的情况下,这个参数返回的数据也是不准确的,甚至有返回null的情况下。
- newSingleThreadExecutor
- 创建一个执行器,该执行器使用单个工作线程在无界队列中操作,注意队列的最大值是Intenger最大值。
- 如果单个由于在关闭之前的执行过程中由于失败而中止,则在需要执行后续任务是,将替换一个新的线程继续执行。
- 保证任务按顺序执行并完成,在任何给定的时间内不会有多个任务处于活跃状态。
- 与其他的执行器不同,他会保证返回的执行器对象不可重新配置以使用额外配置的线程。
- newCachedThreadPool
- 创建一个线程池,线程池中的线程书数量根据当前需要进行创建,并且会重用之前的构造线程。
- 这样的线程池可以提高短时间内较多的异步执行程序的性能。
- 对于没有可用线程的情况,则会创建线程并将其添加到线程池中,并且具有60s呗未使用的将被终止并移除缓存。因此,一个足够长时间的闲置线程池将不会消耗过多的资源。
- newScheduledThreadPool
- 创建一个线程池,该线程池可以安排命令在给定的延迟后运行或定期执行。
- newSingleThreadScheduledExecutor
- 同上,但是线程池内线程数量唯一,唯一性与newSingleThreadExecutor相同。
推荐线程池的使用方式
阿里为大家提供的java开发规约上明确表明了
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1. FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2. CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
既然大佬们都这样说了,那我们就只能自己来通过ThreadPoolExecutor来实现线程池来了,那么接下来看看使用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(核心线程数):池中要保留的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut。
maximumPoolSize(最大线程池大小):池中允许的最大并行执行的线程数。
keepAliveTime(保持活跃时间):当线程池中的活跃线程数大于核心线上数时,这个时间是多余的空闲线程在终止前等待新任务的最长时间(看大佬们描述的多么简洁)。
unit(时间单位):_keepAliveTime的时间单位。使用的是TimeUnit类。
workQueue(工作队列):小伙伴们也发现了,之前Executors类为我们提供好的线程池工具类中,其实主要就是对这个工作队列的疯狂操作吧,我们再看看大佬是怎么说的吧——‘在任务执行前用于保留任务的队列。此队列将只保存{@code execute}方法提交的{@code Runnable}任务’。
ThreadFactory(线程工厂):执行器创建新线程时要使用的工厂,工厂的意义大家可能比我更懂,这里的好处应该就是规范了线程对象输出,添加一个线程名称啊之类的。
handler(处理器):其实就是处理线程任务的一种策略,当执行因达到线程界限和队列容量而被阻止时要使用的处理程序
真正值得注意的其实是各个关键字之间的关系corePoolSize、maximumPoolSize、workQueue这个之前我一直理解的都不正确,看下面这部分代码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
/*
* 下面拢共分了三部分,简单来说一下:
* 1.当 工作线程数 < corePoolSize(核心线程数)时,
* 执行addWorker方法,成功即返回,失败即继续。
*
* 2.当 任务添加队列成功,会两次检验线程是否被停止,有必要的话就停止排队,拒绝执行;
* 或者启动一个新线程去执行。
*
* 3.核心线程满了,队列满了,就会创建新的线程执行,知道达到最大线程数,否则的话
* 会执行拒绝策略。
*/
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);
}
上面的内容可以看出,线程池中的真实执行的顺序是 核心线程>工作队列>最大线程数线程
缓冲队列的使用方式
常用的线程池队列有几种,SynchronousQueue、LinkedBlockingQueue 和ArrayBlockingQueue,下面我们来一一了解他们的具体作用
1.SynchronousQueue 是一种无缓冲的等待队列,它别之处在于内部没有容器,一个任务被put后。就要等待被take掉才能继续消费,这样的过程也有人说是一种配对过程。newCachedThreadPool这种预设线程池对象就用的这种队列。
2.LinkedBlockingQueue 是一种无界缓存等待队列,内部由单链表实现。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。需要注意的是,虽然这个队列通常称其为一个无界队列,但是可以人为的指定队列的大小,而且用于记录队列大小的参数字段未int,所以即使不指定队列的大小,队列的最大值也为Integer.MAX_VALUE。newFixedThreadPool这种预设线程池对象就用的这种队列。
3.ArrayBlockingQueue 是一个邮件缓冲等待队列,他是一个基于数组的阻塞队列。可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入,会根据指定的策略执行。