美文网首页
## 线程池使用需要注意的问题:

## 线程池使用需要注意的问题:

作者: 码而优则仕 | 来源:发表于2020-08-09 20:29 被阅读0次

线程池使用需要注意的问题:

Java 中的 Executors 类定义了一些快捷的工具方法,来帮助我们快速创建线程池。《阿里巴巴 Java 开发手册》中提到,禁止使用这些方法来创建线程池,而应该手动 new ThreadPoolExecutor 来创建线程池。这一条规则的背后,是大量血淋淋的生产事故,最典型的就是 newFixedThreadPool 和 newCachedThreadPool,可能因为资源耗尽导致 OOM 问题。

虽然使用 newFixedThreadPool 可以把工作线程控制在固定的数量上,但任务队列是无界的。如果任务较多并且执行较慢的话,队列可能会快速积压,撑爆内存导致 OOM。

Executors.newFixedThreadPool(1);
//核心线程和最大线程数量都设置为1 ,keepAliveTime 0,工作队列使用LinkedBlockingQueue,容量为Integer.MAX_VALUE即相当于无限大。不设置线程工程和拒绝策略,使用默认的。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
 public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

翻看 newCachedThreadPool 的源码可以看到,这种线程池的最大线程数是 Integer.MAX_VALUE,可以认为是没有上限的,而其工作队列 SynchronousQueue 是一个没有存储空间的阻塞队列。这意味着,只要有请求到来,就必须找到一条工作线程来处理,如果当前没有空闲的线程就再创建一条新的。所以如果任务提交频率比较高,就会疯狂创建线程导致OOM

Executors.newCachedThreadPool();
//核心线程设置为0,最大线程设置为 Integer.MAX_VALUE 意味着无限大,keepAliveTime 60,工作队列SynchronousQueue 同步队列,容量为0。不设置线程工程和拒绝策略,使用默认的。
 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
     public SynchronousQueue() {
        this(false);
    }
    
    

因此,不建议使用 Executors 提供的两种快捷的线程池,原因如下:

  • 我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数。任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题。

  • 我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数。任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题。

  • 除了建议手动声明线程池以外,还建议用一些监控手段来观察线程池的状态。线程池这个组件往往会表现得任劳任怨、默默无闻,除非是出现了拒绝策略,否则压力再大都不会抛出一个异常。如果我们能提前观察到线程池队列的积压,或者线程数量的快速膨胀,往往可以提早发现并解决问题。

线程池默认的工作行为:

  • 不会初始化 corePoolSize 个线程,有任务来了才创建工作线程;

  • 当核心线程满了之后不会立即扩容线程池,而是把任务堆积到工作队列中;

  • 当工作队列满了后扩容线程池,一直到线程个数达到 maximumPoolSize 为止;

  • 如果队列已满且达到了最大线程后还有任务进来,按照拒绝策略处理;

  • 当线程数大于核心线程数时,线程等待 keepAliveTime 后还是没有任务需要处理的话,收缩线程到核心线程数。

Executors.newFixedThreadPool(1);由于工作队列无限大,所以当需要处理的任务进来没有空闲的核心线程时,直接将任务放置到工作队列中,所以核心线程数量一定等于最大线程数量;因为最大线程数量是在核心用完后,工作队列也放完之后才会增加线程到最大线程数量。

Executors.newCachedThreadPool();工作队列是没有容量的阻塞同步队列即工作队列中放不下任何任务,所以任务进来只能创建新线程执行,所以最大线程数量是近乎无限大的Integer.MAX_VALUE。核心线程数量直接设置0。60秒后线程回收。

不知道你有没有想过:Java 线程池是先用工作队列来存放来不及处理的任务满了之后再扩容线程池。当我们的工作队列设置得很大时,最大线程数这个参数显得没有意义,因为队列很难满,或者到满的时候再去扩容线程池已经于事无补了。

那么,我们有没有办法让线程池更激进一点,优先开启更多的线程,而把队列当成一个后备方案呢?

限于篇幅,这里我只给你一个大致思路:

  • 由于线程池在工作队列满了无法入队的情况下会扩容线程池,那么我们是否可以重写队列的 offer 方法,造成这个队列已满的假象呢?

  • 由于我们 Hack 了队列,在达到了最大线程后势必会触发拒绝策略,那么能否实现一个自定义的拒绝策略处理程序,这个时候再把任务真正插入队列呢?

接下来,就请你动手试试看如何实现这样一个“弹性”线程池吧。Tomcat 线程池也实现了类似的效果,可供你借鉴。

要根据任务的“轻重缓急”来指定线程池的核心参数,包括线程数、回收策略和任务队列:

  • 对于执行比较慢、数量不大的 IO 任务,或许要考虑更多的线程数,而不需要太大的队列。

  • 而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是 CPU 核数或核数 *2(理由是,线程一定调度到某个 CPU 进行执行,如果任务本身是 CPU 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲。

相关文章

  • 线程池使用FutureTask时候需要注意的一点事

    8.4 线程池使用FutureTask时候需要注意的一点事 线程池使用FutureTask的时候如果拒绝策略设置为...

  • ## 线程池使用需要注意的问题:

    线程池使用需要注意的问题: Java 中的 Executors 类定义了一些快捷的工具方法,来帮助我们快速创建线程...

  • ExecutorService shutdown()和shutd

    ExecutorService是我们经常使用的线程池,当我们使用完线程池后,需要关闭线程池。ExecutorSer...

  • java线程池基础与原理

    为什么使用线程池 在没有线程池的场景,我们使用多线程时: 线程池的创建和销毁需要消耗额外的资源。线程的创建需要开辟...

  • 八、线程池剖析

    一、前置问题 线程的状态转换 为什么要使用线程池 线程池的继承体系 线程池使用的场景 线程数的设置规则 线程池的状...

  • 1-50

    1threadlocal使用时注意的问题 线程池使用ThreadLocal, 会获取到上个用户的数据. 退...

  • 一篇文章搞懂线程池

    线程池 什么使用使用线程池? 单个任务处理时间比较短 需要处理的任务数量很大 线程池优势 重用存在的线程,减少线程...

  • java并发基础-线程池

    线程池主要解决两个问题:当执行大量异步任务时线程池能够提供较好的性能。在不使用线程池时,每当需要执行异步任务时直接...

  • InheritableThreadLocal还存在的问题

    [toc] 问题场景 我们使用线程时候往往不会只是简单的new Thread对象,而是使用线程池,当然使用线程池的...

  • Java并发包中线程池ThreadPoolExecutor原理探

    一、线程池简介 线程池的使用主要是解决两个问题:①当执行大量异步任务的时候线程池能够提供更好的性能,在不使用线程池...

网友评论

      本文标题:## 线程池使用需要注意的问题:

      本文链接:https://www.haomeiwen.com/subject/rtdrdktx.html