为什么要使用线程池?
- 减少不必要的资源消耗,将线程交由线程池管理,可以有效的避免线程的频繁创建与销毁。
- 可以有效的对线程的数量进行控制,避免创建过多的线程导致系统资源被大量消耗,反而降低系统的响应时间。
JDK中线程池的核心参数定义
corePoolSize
核心线程数,即线程池始终保持着corePoolSize个线程数。
maximumPoolSize
线程池中最多允许创建maximumPoolSize个线程。
keepAliveTime
假设corePoolSize是5,maximumPoolSize是6,相当于有1个线程是非核心线程,那么当这个线程空闲了keepAliveTime时间后,将被销毁。
workQueue
这是一个阻塞队列,用于存放当前线程来不及处理的任务。
threadFactory
创建线程的工厂,为每个线程起一个有意思的名称,方便问题排查。
handler
拒绝策略,定义如果阻塞队列被放满了以后,接下来的任务如何处理。
线程池的工作流程图
拒绝策略
拒绝策略只定义了一个接口,一个方法,由使用者根据自身情况自动实现。
/**
* A handler for tasks that cannot be executed by a {@link ThreadPoolExecutor}.
*
* @since 1.5
* @author Doug Lea
*/
public interface RejectedExecutionHandler {
/**
* Method that may be invoked by a {@link ThreadPoolExecutor} when
* {@link ThreadPoolExecutor#execute execute} cannot accept a
* task. This may occur when no more threads or queue slots are
* available because their bounds would be exceeded, or upon
* shutdown of the Executor.
*
* <p>In the absence of other alternatives, the method may throw
* an unchecked {@link RejectedExecutionException}, which will be
* propagated to the caller of {@code execute}.
*
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
JDK自带的几种拒绝策略
- AbortPolicy
JDK自带线程池中默认的拒绝策略,直接拒绝任务并抛出异常。 - CallerRunsPolicy
由当前调用调用者继续执行当前任务。 - DiscardPolicy
直接丢弃当前任务 - DiscardOldestPolicy
丢弃阻塞队列中最老的任务
其他一些框架中的拒绝策略
- NewThreadRunsPolicy
这是Netty中的拒绝策略,和CallerRunsPolicy有点像,任务不会丢弃,不同的是Netty中是新建了一个线程继续执行当前任务。
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
final Thread t = new Thread(r, "Temporary task executor");
t.start();
} catch (Throwable e) {
throw new RejectedExecutionException(
"Failed to start a new thread", e);
}
}
}
- AbortPolicyWithReport
dubbo中的拒绝策略,也是抛出异常,不同的时对于日志内容的输出更加丰富,也是为了我们更好的排查问题。
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED!" +
" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!" ,
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg);
throw new RejectedExecutionException(msg);
}
- EsAbortPolicy
elasticsearch中的方式,从源码中可以看出,只有当isForceExecution为true,且阻塞队列是SizeBlockingQueue类型时,才会放入当前队列中,否则抛出异常。
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r instanceof AbstractRunnable) {
if (((AbstractRunnable) r).isForceExecution()) {
BlockingQueue<Runnable> queue = executor.getQueue();
if (!(queue instanceof SizeBlockingQueue)) {
throw new IllegalStateException("forced execution, but expected a size queue");
}
try {
((SizeBlockingQueue) queue).forcePut(r);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("forced execution, but got interrupted", e);
}
return;
}
}
rejected.inc();
throw new EsRejectedExecutionException("rejected execution of " + r + " on " + executor, executor.isShutdown());
}
- ActiveMQ
ActiveMQ的思想是如果队列满了,则尝试等待1分钟的时间,1分钟内如果队列还是满的,那么就抛出异常。
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
try {
executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
}
throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
}
如果JDK自带的拒绝策略不能满足业务需求,那么大家就可以借鉴这些第三方框架的思想和自己的业务实现自定义的拒绝策略了。
JDK自带的4种线程池比较
如何合理的配置线程池?
既然JDK自带的线程池我们一般都不会去使用,那么我们就要自己创建线程池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
核心线程数
一般在配置核心线程数的时候,是需要结合线程池将要处理任务的特性来决定的。
任务的性质一般可以划分为:CPU密集型、IO密集型
CPU密集型:一般建议线程的核心数与CPU核心数保持一致。
IO密集型:一般可以设置2倍的CPU核心数的线程数,因为此类任务CPU比较空闲,可以多分配点线程充分利用CPU资源来提高效率。
通过Runtime.getRuntime().availableProcessors()可以获取核心线程数。
另外还有一个公式可以借鉴:
线程核心数 = cpu核心数 / (1-阻塞系数)
阻塞系数 = 阻塞时间/(阻塞时间+使用CPU的时间)
阻塞队列的设置多大合适?
队列的长度一般要根据处理任务的速度与任务产生的速度进行计算得到一个大概的数值。
假设现在有1个线程,每秒钟可以处理10个任务,正常情况下每秒钟产生的任务数小于10,那么此时队列长度为10就足以。
但是如果高峰时期,每秒产生的任务数会达到20,会持续10秒,且任务又不希望丢弃,那么此时队列的长度就需要设置到100。
最大线程数
这个参数建议与核心线程数保持一致即可,也就是只通过阻塞队列来进行任务执行的控制,当超过队列容量时,直接执行拒绝策略即可。