Java线程池参数分析

前言

在阿里开发手册中,对于线程池的使用有这样两点要求:
在这里插入图片描述
正确的使用线程池可以减少在创建和销毁线程上所消耗的时间,所以大家都知道在使用线程时,应该构建一个线程池,然后从线程池中取线程,尽管JDK已经为我们提供了线程池,不必我们重复造轮子,但项目中还是会因为在构建线程池时,因为参数设置不正确导致生产上出现问题,本文就来分析一下JDK为我们提供的线程池中每个参数的含义以及应该如何决定设置什么样的值。

JDK中线程池的核心参数定义

corePoolSize

核心线程数,即线程池始终保持着corePoolSize个线程数。

maximumPoolSize

线程池中最多允许创建maximumPoolSize个线程。

keepAliveTime

假设corePoolSize是5,maximumPoolSize是6,相当于有1个线程是非核心线程,那么当这个线程空闲了keepAliveTime时间后,将被销毁。

workQueue

这是一个阻塞队列,用于存放当前线程来不及处理的任务。

threadFactory

创建线程的工厂,为每个线程起一个有意思的名称,方便问题排查。

handler

拒绝策略,定义如果阻塞队列被放满了以后,接下来的任务如何处理。

线程池的工作流程图

在这里插入图片描述

这里的流程特别要注意,maximumPoolSize是在阻塞队列满了以后才去检查的。

参数配置建议

corePoolSize(核心线程数)

我们先来看看核心线程数,一般在配置核心线程数的时候,是需要结合线程池将要处理任务的特性来决定的,而任务的性质一般可以划分为:CPU密集型、IO密集型。

比较通用的配置方式如下:

CPU密集型:一般建议线程的核心数与CPU核心数保持一致。
IO密集型:一般可以设置2倍的CPU核心数的线程数,因为此类任务CPU比较空闲,可以多分配点线程充分利用CPU资源来提高效率。
通过Runtime.getRuntime().availableProcessors()可以获取核心线程数。

另外还有一个公式可以借鉴:

线程核心数 = cpu核心数 / (1-阻塞系数)
阻塞系数 = 阻塞时间/(阻塞时间+使用CPU的时间)

实际上如果你做的是一个web项目,大多数时间都集中在IO等待上,那么线程数是可以开的再大一点的,比如tomcat中默认的线程数就200。

所以最佳的核心线程数是需要根据特定场景,然后通过实际上的测试分析后,再结合实际生产环境,不断的调整中决定的。

maximumPoolSize

对于这个参数,我们一般建议设置的和核心线程数一样就可以了,避免造成性能不稳定,我们更倾向于通过阻塞队列和拒绝策略来控制。

keepAliveTime

maximumPoolSize不会再创建线程,自然这个参数也没什么用了。

workQueue

那么阻塞队列应该设置多大呢?我们知道当线程池中所有的线程都在工作时,如果再有任务进来,就会被放到阻塞队列中等待,如果阻塞队列设置的太小,可能很快队列就满了,导致任务被丢弃或者异常(由拒绝策略决定),如果队列设置的太大,又可能会带来内存资源的紧张,甚至OOM,以及任务延迟时间过长。

所以阻塞队列的大小,又是要结合实际场景来设置的。

一般会根据处理任务的速度与任务产生的速度进行计算得到一个大概的数值。

假设现在有1个线程,每秒钟可以处理10个任务,正常情况下每秒钟产生的任务数小于10,那么此时队列长度为10就足以。
但是如果高峰时期,每秒产生的任务数会达到20,会持续10秒,且任务又不希望丢弃,那么此时队列的长度就需要设置到100。

监控workQueue中等待任务的数量是非常重要的,只有了解实际的情况,才能做出正确的决定。

threadFactory

通过threadFactory我们可以自定义线程组的名字,这在阿里开发手册中也是强制的要求。

在这里插入图片描述

见名知意在排查问题时是非常重要的

handler

最后拒绝策略,这也是要结合实际的业务场景来决定采用什么样的拒绝方式。

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自带的拒绝策略不能满足业务需求,那么大家就可以借鉴这些第三方框架的思想和自己的业务实现自定义的拒绝策略了。

总结

通过上面的介绍可以看出,线程池中的参数,几乎都要结合实际的业务场景来指定,并且还需要通过不断的监控来进行调整,只有这样才能正确、有效的发挥出线程池的作用。

好了,文章结束了,希望对你有所帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码拉松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值