管理一系列线程的资源池。处理完毕后线程不会立即销毁,而是等待下一次任务使用。
复用线程降低资源消耗、使用现成的线程减少创建等待时间、提高线程的可管理性。
1. 通过ThreadPoolExecutor构造方法创建
2. 通过Executors提供的方法创建
Integer.MAX_VALUE,如果等待队列堆积大量未处理的请求对象,就可能导致OOM。
Integer.MAX_VALUE,如果任务数量过多且执行较慢(一直没有空闲线程就会一直创建新线程),可能会创建过多的线程导致OOM。
ScheduledThreadPool
和 SingleThreadScheduledExecutor
使用延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为 Integer.MAX_VALUE,因其
永远不会创建超过 corePoolSize
数量的线程来执行任务,可能堆积大量请求导致OOM
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue 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;
}
核心的三个参数:
其他常见参数:
一般不会,因为要减少线程创建的开销,核心线程需要保持长期活跃。但如果线程池是被用于周期性的场景,且频率不高(周期较长),可以考虑将 allowCoreThreadTimeOut(boolean value)
方法的参数设置为 true,这样就会回收超时空闲的核心线程。
若有可用任务,会从WAITING变为RUNNABLE。
线程池内部实现原理:
线程在线程池内部被抽象为了Worker,当Worker被启动后会不断从任务队列中获取任务。
获取任务时,会通过一个timed的布尔值来判断从任务队列获取任务的行为。
如果设置了「核心线程的存活时间」或「当前线程数量超过核心线程数量」,则将timed标记为true,表示获取任务时需要使用poll()来指定最大阻塞等待时间。
timed == true
:使用 poll()
来获取任务。使用 poll()
方法获取任务超时的话,则当前线程会退出执行( TERMINATED
),该线程从线程池中被移除。timed == false
:使用 take()
来获取任务。使用 take()
方法获取任务会让当前线程一直阻塞等待(WAITING
)。// ThreadPoolExecutor
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
// ...
// 1、如果「设置了核心线程的存活时间」或者是「线程数量超过了核心线程数量」,则 timed 为 true。
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 2、扣减线程数量。
// wc > maximuimPoolSize:线程池中的线程数量超过最大线程数量。其中 wc 为线程池中的线程数量。
// timed && timeOut:timeOut 表示获取任务超时。
// 分为两种情况:核心线程设置了存活时间 && 获取任务超时,则扣减线程数量;线程数量超过了核心线程数量 && 获取任务超时,则扣减线程数量。
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 3、如果 timed 为 true,则使用 poll() 获取任务;否则,使用 take() 获取任务。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 4、获取任务之后返回。
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
如果当前等待任务队列和存活线程数量都达到最大值,ThreadPoolExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出 RejectedExecutionException
来拒绝新任务的处理。这个是默认的拒绝策略。execute()
或 submit()
方法的那个线程)来直接执行这个被拒绝的任务。可能会使提交任务的线程阻塞从而降低新任务提交速度。ThreadPoolExecutor.DiscardPolicy
:不处理新任务也不抛异常,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
:此策略将丢弃workQueue中最早的未处理的任务请求,然后将新任务添加到工作队列中。如果要处理的是一个非常耗时的任务,而提交任务的又是主线程,就会导致主线程长时间阻塞,影响后续的任务提交,影响程序的正常运行,导致数据在源头堆积造成OOM。
为了不使提交任务的线程阻塞,我们可以增加阻塞队列BlockingQueue
的大小并调整堆内存以容纳更多的任务,确保任务能够被准确执行。也可以增加调整线程池的maximumPoolSize。
但是,如果服务器资源以达到可利用的极限,无法增加最大线程数量和阻塞队列的大小,我们的思路需要转为任务持久化。方法包括但不限于:
线程池在提交任务前,可以提前创建线程吗?
答案是可以的。ThreadPoolExecutor
提供了两个方法帮助我们在提交任务之前,完成核心线程的创建,从而实现线程池预热的效果:
prestartCoreThread()
:启动一个核心线程,等待任务,如果已达到核心线程数,这个方法返回 false,否则返回 true;prestartAllCoreThreads()
:启动所有的核心线程,并返回启动成功的核心线程数。分两种情况:
使用execute()提交任务:如果任务的异常未被捕获并抛出,那么该异常会导致线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到线程终止,然后创建一个新的线程替换它。
使用submit()提交任务:如果在任务执行中发生异常,这个异常不会直接打印出来。异常会被封装到submit()返回的Future对象中,调用Future.get()方法可以捕获到一个ExecuteException。这种情况下线程不会异常终止,会继续存在于线程池中等待复用。
默认名字是pool-1-thread-n
1. 使用guava的ThreadFactoryBuilder:
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat(threadNamePrefix + "-%d")
.setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
2. 自定义ThreadFactory
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 线程工厂,它设置线程名称,有利于我们定位问题。
*/
public final class NamingThreadFactory implements ThreadFactory {
private final AtomicInteger threadNum = new AtomicInteger();
private final String name;
/**
* 创建一个带名字的线程池生产工厂
*/
public NamingThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
return t;
}
}
主要是对corePoolSize、maximumPoolSize和workQueue进行动态修改,通过ThreadPoolExecutor的set方法。
程序运行期间的时候,我们调用 setCorePoolSize()
这个方法的话,线程池会首先判断当前工作线程数是否大于newCorePoolSize
,如果大于的话就会对超出数量的线程设置超时时间逐渐回收工作线程。
ThreadPoolExecutor并没有动态指定工作队列长度的方法,美团的方式是自定义了一个ResizableCapacityLinkedBlockIngQueue
的队列(主要就是把LinkedBlockingQueue
的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
使用PriorityBlockingQueue优先级阻塞队列,值最小的元素优先出队。作为参数传入ThreadPoolExecutor中。
队列中的任务必须有排序能力,可以让任务本身实现Comparable接口或者新建一个Comparator对象以供创建PriorityBlockingQueue时传入。
但是会有一些风险和问题:
ReentrantLock
),因此会降低性能。OOM的问题很好解决,就在PriorityBlockingQueue的实现中重写一下offer()方法,插入元素数量超过指定值就返回false。
排序和线程安全问题无法避免。
饥饿问题:等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。