线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式

引言

《Java 开发手册》 编程规约|并发处理中指出

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

规约中提到java.util.concurrent.ThreadPoolExecutor,我们来看下它的构造方法

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

参数说明

参数名说明
corePoolSize线程池维护线程的最少数量。线程池至少会保持改数量的线程存在,即使没有任务可以处理。(注意:这里说的至少是指线程达到这个数量后,即使有空闲的线程也不会释放,而不是说线程池创建好之后就会初始化这么多线程)
maximumPoolSize池中允许的最大线程数
keepAliveTime线程池维护线程所允许的空闲时间。当线程池中的线程数量大于 corePoolSize时,超过corePoolSize的线程如果空闲时间超过keepAliveTime,线程将被终止
unitkeepAliveTime参数的时间单位
workQueue在执行任务之前用于保留任务的队列。 此队列将仅保存execute方法提交的Runnable任务。
threadFactory执行程序创建新线程时要使用的工厂,java.util.concurrent.Executors.DefaultThreadFactory
handler线程池对拒绝任务的处理策略。因达到线程界限和队列容量而被阻止执行时使用的处理程序.AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy、自定义

BlockingQueue类型

参数名说明
LinkedBlockingQueue无界队列,FIFO(先进先出),可以无限向队列中添加任务,直到内存溢出,newSingleThreadExecutor、newFixedThreadPool
ArrayBlockingQueue有界队列,FIFO,需要指定队列大小,如果队列满了,会触发线程池的RejectedExecutionHandler逻辑
SynchronousQueue一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。可以简单理解为是一个容量只有1的队列。Executors.newCachedThreadPool使用的是这个队列newCachedThreadPool
PriorityBlockingQueue优先级队列,线程池会优先选取优先级高的任务执行,队列中的元素必须实现Comparable接口
DelayedWorkQueue专门的延迟队列。 为了与TPE声明相啮合,必须将此类声明为BlockingQueue 即使它只能容纳RunnableScheduledFutures。newScheduledThreadPool

RejectedExecutionHandler类型

参数名说明
AbortPolicy中止政策。线程池默认的策略,如果元素添加到线程池失败,会抛出RejectedExecutionException异常
DiscardPolicy丢弃政策。如果添加失败,则放弃,并且不会抛出任何异常(空实现)
DiscardOldestPolicy放弃最早的政策。如果添加到线程池失败,会将队列中最早添加的元素移除,再尝试添加,如果失败则按该策略不断重试
CallerRunsPolicy除非执行器已关闭,否则在调用者线程中执行任务r(使用run方法),在这种情况下,该任务将被丢弃。
自定义如果觉得以上几种策略都不合适,那么可以自定义符合场景的拒绝策略。需要实现RejectedExecutionHandler接口,并将自己的逻辑写在rejectedExecution方法内。

原理

有请求时,创建线程执行任务,当线程数量等于corePoolSize时,请求加入阻塞队列里,当队列满了时,接着创建线程,线程数等于maximumPoolSize。 当任务处理不过来的时候,线程池开始执行拒绝策略。

Java中Executors提供了4种线程池

newSingleThreadExecutor

创建一个单线程的线程池,该执行程序使用单个工作线程在不受限制的队列中操作。 (但是请注意,如果单个线程由于在关闭之前执行期间由于执行失败而终止,则新线程将在需要执行后续任务时取而代之。)保证任务按顺序执行,且最多执行一个任务将在任何给定时间处于活动状态

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

newFixedThreadPool

创建一个固定数量的线程池,该线程池可重用固定数量的线程在共享的无界队列上操作。在任何时候,最多 {@code nThreads}个线程将是活动的处理任务。 如果在所有线程都处于活动状态时提交了其他任务,则它们将在队列中等待,直到某个线程可用为止。 如果任何线程由于执行过程中的失败而终止在关闭之前如果需要执行一个新任务,则将替换一个新线程。直到显式{@link ExecutorService#shutdown shutdown}之前,池中的线程将一直存在。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

newCachedThreadPool

创建一个线程池,该线程池根据需要创建新线程,但是会在可用时重用以前构造的线程。这些池通常将提高执行许多短期异步任务的程序的性能。 调用{@code execute}将重用以前构造的线程(如果有)。如果没有可用的现有线程,则将创建一个新的线程并将其添加到池中。 六十秒未使用的线程将终止并从缓存中删除。因此,保持空闲时间足够长的池将不会消耗任何资源。请注意,可以使用{@link ThreadPoolExecutor}构造函数创建具有类似属性但不同详细信息(例如,超时参数)的池。

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

newScheduledThreadPool

创建一个线程池,该线程池可以计划命令在给定的延迟后运行或定期执行。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

自定义ThreadFactory

  • 规约中已提供
/**
 * 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
 * 
 * 正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给 whatFeaturOfGroup
 * 
 * @author ylm-sigmund
 * @since 2020/12/15 19:42
 */
public class UserThreadFactory implements ThreadFactory {
    private static final Logger LOGGER = LoggerFactory.getLogger(UserThreadFactory.class);
    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger(1);

    /**
     * 定义线程组名称,在 jstack 问题排查时,非常有帮助
     * 
     * @param whatFeatureOfGroup
     *            whatFeatureOfGroup
     */
    public UserThreadFactory(String whatFeatureOfGroup) {
        namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-Worker-";
    }

    /**
     * 创建线程
     * 
     * @param task
     *            Runnable
     * @return Thread
     */
    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix + nextId.getAndIncrement();
        Thread thread = new Thread(task, name);
        LOGGER.info("UserThreadFactory newThread'name={}", thread.getName());
        return thread;
    }
}

自定义RejectedExecutionHandler

RejectedExecutionHandler customRejectedExecutionHandler = (Runnable runnable, ThreadPoolExecutor executor) -> {
            LOGGER.error(
                "customRejectedExecutionHandler:The thread pool is full and the task is discarded,ThreadPoolExecutor={}",
                executor.toString());
        };

合理配置线程池大小

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

任务的性质:CPU密集型任务,IO密集型任务和混合型任务。

任务的优先级:高,中和低。

任务的执行时间:长,中和短。

任务的依赖性:是否依赖其他系统资源,如数据库连接。

根据任务所需要的cpu和io资源的量可以分为,

CPU密集型任务: 主要是执行计算任务,响应时间很快,cpu一直在运行,这种任务cpu的利用率很高。

IO密集型任务:主要是进行IO操作,执行IO操作的时间较长,这是cpu出于空闲状态,导致cpu的利用率不高。

为了合理最大限度的使用系统资源同时也要保证的程序的高性能,可以给CPU密集型任务和IO密集型任务配置一些线程数。

CPU密集型:线程个数为CPU核数。这几个线程可以并行执行,不存在线程切换到开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗

IO密集型:线程个数为CPU核数的两倍。到其中的线程在IO操作的时候,其他线程可以继续用cpu,提高了cpu的利用率。

返回可用于Java虚拟机的处理器数量。 在虚拟机的特定调用期间,此值可能会更改。 因此,对可用处理器数量敏感的应用程序应该偶尔轮询此属性并适当地调整其资源使用情况。
返回值: CPU核数,虚拟机可用的最大处理器数量; 永远不小于一个
Runtime.getRuntime().availableProcessors();

资料参考

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
jstack生成的Thread Dump日志.docx 系统线程状态 (Native Thread Status) 系统线程有如下状态: deadlock 死锁线程,一般指多个线程调用期间进入了相互资源占用,导致一直等待无法释放的情况。 runnable 一般指该线程正在执行状态中,该线程占用了资源,正在处理某个操作,如通过SQL语句查询数据库、对某个文件进行写入等。 blocked 线程正处于阻塞状态,指当前线程执行过程中,所需要的资源长时间等待却一直未能获取到,被容器的线程管理器标识为阻塞状态,可以理解为等待资源超时的线程。 waiting on condition 线程正处于等待资源或等待某个条件的发生,具体的原因需要结合下面堆栈信息进行分析。 (1)如果堆栈信息明确是应用代码,则证明该线程正在等待资源,一般是大量读取某种资源且该资源采用了资源锁的情况下,线程进入等待状态,等待资源的读取,或者正在等待其他线程的执行等。 (2)如果发现有大量的线程都正处于这种状态,并且堆栈信息中得知正等待网络读写,这是因为网络阻塞导致线程无法执行,很有可能是一个网络瓶颈的征兆: 网络非常繁忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写; 网络可能是空闲的,但由于路由或防火墙等原因,导致包无法正常到达; 所以一定要结合系统的一些性能观察工具进行综合分析,比如netstat统计单位时间的发送包的数量,看是否很明显超过了所在网络带宽的限制;观察CPU的利用率,看系统态的CPU时间是否明显大于用户态的CPU时间。这些都指向由于网络带宽所限导致的网络瓶颈。 (3)还有一种常见的情况是该线程在 sleep,等待 sleep 的时间到了,将被唤醒。 waiting for monitor entry 或 in Object.wait() Moniter 是Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者class的锁,每个对象都有,也仅有一个 Monitor。 从上图可以看出,每个Monitor在某个时刻只能被一个线程拥有,该线程就是 "Active Thread",而其他线程都是 "Waiting Thread",分别在两个队列 "Entry Set"和"Waint Set"里面等待。其中在 "Entry Set" 中等待的线程状态是 waiting for monitor entry,在 "Wait Set" 中等待的线程状态是 in Object.wait()。 (1)"Entry Set"里面的线程。 我们称被 synchronized 保护起来的代码段为临界区,对应的代码如下: synchronized(obj){} 当一个线程申请进入临界区时,它就进入了 "Entry Set" 队列中,这时候有两种可能性: 该Monitor不被其他线程拥有,"Entry Set"里面也没有其他等待的线程。本线程即成为相应类或者对象的Monitor的Owner,执行临界区里面的代码;此时在Thread Dump中显示线程处于 "Runnable" 状态。 该Monitor被其他线程拥有,本线程在 "Entry Set" 队列中等待。此时在Thread Dump中显示线程处于 "waiting for monity entry" 状态。 临界区的设置是为了保证其内部的代码执行的原子性和完整性,但因为临界区在任何时间只允许线程串行通过,这和我们使用多线程的初衷是相反的。如果在多线程程序中大量使用synchronized,或者不适当的使用它,会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。如果在Thread Dump中发现这个情况,应该审视源码并对其进行改进。 (2)"Wait Set"里面的线程 当线程获得了Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(通常是被synchronized的对象)的wait()方法,放弃Monitor,进入 "Wait Set"队列。只有当别的线程在该对象上调用了 notify()或者notifyAll()方法,"Wait Set"队列中的线程才得到机会去竞争,但是只有一个线程获得对象的Monitor,恢复到运行态。"Wait Set"中的线程在Thread Dump中显示的状态为 in Object.wait()。通常来说, 通常来说,当CPU很忙的时候关注 Runnable 状态的线程,反之则关注 waiting for monitor entry 状态的线程。 JVM线程运行状态 (JVM Thread Status)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值