你要知道为什么不推荐使用 Executors 创建线程池吗?

阿里开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式
其实之前也听别人提起过不要使用Executors来创建线程池?为什么呢?当时自己也没有想过,现如今看到了线程池源码才有所明白。
下面就通过解读源码来分析以下,为什么阿里开发手册上有这么一条规定。

写在前面
  • 线程池是什么
  • Executors创建线程池的方式
  • ThreadPoolExecutor是什么
  • 线程池参数解密
  • OOM异常测试
  • 为什么阿里禁止使用Executors 创建线程池
  • 创建线程池正确的姿势
线程池是什么

线程池是管理我们工作线程的。为什么要有线程池?
1、为了减少创建和销毁线程的次数。频繁的去创建、销毁线程是需要成本的
2、提高稳定稳定性,避免无限创建线程引起的OOM

Executors创建线程池的方式

通过Executors来创建线程池,JDK默认为我们提供了4种线程池:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ThreadPoolExecutor是什么

没看过源码的同学这个时候就有点不想看这篇文章了。不是说好了要讲Executors ,怎么又扯到ThreadPoolExecutor身上了?
楼主赶紧告诉大家,JDK提供的四种线程池底层都是通过ThreadPoolExecutor来创建的。看两种截图:
在这里插入图片描述在这里插入图片描述这两个线程池都是调用了ThreadPoolExecutor这个类,下面我们看一下这个类:

public ThreadPoolExecutor(int corePoolSize,  // 线程池的核心线程数
                         int maximumPoolSize, // 线程池的最大线程数
                        long keepAliveTime, // 当线程数大于核心时,多余的空闲线程等待新任务的存活时间。
                         TimeUnit unit, // keepAliveTime的时间单位
                         ThreadFactory threadFactory, // 线程工厂
                         BlockingQueue<Runnable> workQueue,// 用来储存等待执行任务的队列
                         RejectedExecutionHandler handler // 拒绝策略
                         ) 

ThreadPoolExecutor种有七个参数,分别代表的意思已经标注出来了。
下面这张图呢就是线程池的运行原理,这个不需要解释,根据下面的线程池参数解释会更好理解:
在这里插入图片描述

线程池参数解密
  • corePoolSize 核心线程数
    当前运行的线程数少于corePoolSize并且新任务没有空闲线程来执行的时候,则添加新的线程执行该任务。
  • BlockingQueue workQueue 阻塞队列
    当前的线程数等于corePoolSize同时workQueue未满并且新任务没有空闲线程来执行的时候,则将任务入队列,而不添加新的线程。对于队列,通常由两种:
    SynchronousQueue
    此队列中不缓存任何一个任务。向线程池提交任务时,如果没有空闲线程来运行任务,则入列操作会阻塞。当有线程来获取任务时,出列操作会唤醒执行入列操作的线程。从这个特性来看,SynchronousQueue是一个无界队列,因此当使用SynchronousQueue作为线程池的阻塞队列时,参数maximumPoolSizes没有任何作用。
    LinkedBlockingQueue
    用链表实现的队列,可以是有界的,也可以是无界的,但在Executors中默认使用无界的。
    ArrayBlockingQueue
    FIFO原则的队列
  • maximumPoolSize 最大线程数
    当前的线程数等于corePoolSize同时workQueue满了并且线程池中的线程数小于最大线程数,则额外创建线程来执行任务。
  • keepAliveTime 空闲线程的存活时间。
    当线程数大于核心线程数的时候,额外线程等待任务的最大时间,如果超过这个时间,额外线程就被销毁
  • TimeUnit unit 表示keepAliveTime的单位。
  • ThreadFactory threadFactory 线程创建的工厂
    为了方便的管理我们的线程,我们可以自定义线程创建工厂,来对我们的线程进行个性化的设置,比如说线程名、优先级等等
  • RejectedExecutionHandler handler 拒绝策略
    当我们核心线程数、阻塞队列、最大线程数都到达上限,这个时候新的任务就会走拒绝策略,那么拒绝策略有哪些呢?通常由以下四种:
    【ThreadPoolExecutor.AbortPolicy() ====抛出RejectedExecutionException异常】
    【ThreadPoolExecutor.CallerRunsPolicy() ====由向线程池提交任务的线程来执行该任务】
    【ThreadPoolExecutor.DiscardOldestPolicy() ====抛弃最旧的任务《最先提交而没有得到执行的任务》】
    【ThreadPoolExecutor.DiscardPolicy() ====抛弃当前的任务】
OOM异常测试

这里做了个小例子,模拟以下使用Executors 来创建线程池为什么会OOM
首先为了方便模拟OOM,我们先调整一下jvm的内存
在这里插入图片描述

    public static void main(String[] args) throws InterruptedException {
     
     
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        while (true) {
     
     
            executorService.execute(new Task());
        }
    }

    static class Task implements Runnable{
     
     
        @Override
        public void run() {
     
     
            try {
     
     
                Thread.sleep(10000);
            } catch (InterruptedException e) {
     
     
                e.printStackTrace();
            }
        }
    } } ```

上面这块代码非常简单,创建一个固定大小的线程池,无限的去执行Task任务,每个任务sleep 10000毫秒。
这个时候去运行,这个时候你就会发现 :GC overhead limit exceeded
在这里插入图片描述OOM,我要怎么去分析是什么情况造成OOM的呢?是因为线程的原因吗?不对,因为我 已经设置了核心线程数为5个,不信来看一看线程堆栈,运行命令:jps找到运行类的pid,执行命令jstack 75716>75716.log
在这里插入图片描述
在这里插入图片描述
后面的就不截图了,大家可以自己试一试。你会发现,线程堆栈里始终保持着5个线程。
那么到底是因为什么原因导致的OOM?其后面究竟隐藏着什么?
这就要说到我们今天的主题,《阿里巴巴为什么要禁用 Executors 创建线程池?》究竟是道德的沦丧,还是人性的毁灭?

为什么阿里禁止使用Executors 创建线程池

在这里插入图片描述
接着上面那个OOM的截图来一行一行的解析(这里不会把所有源码看一遍):
最后一行:这里定位到了我自己写的代码,executorService.execute(new Task());
这里其实就是执行了execute方法。我们跟踪execute方法,其实调用的是ThreadPoolExecutor的execute。
原因上面讲到过,通过Executors来创建的四种线程池都是调用了ThreadPoolExecutor里的方法。我们创建的是固定线程数的线程池newFixedThreadPool。
我们继续去看ThreadPoolExecutor的execute方法,jvm已经帮我们定位到了ThreadPoolExecutor的1371行,我们去看1371行就在ThreadPoolExecutor的execute方法里
在这里插入图片描述在if判断里,前面的判断是判断线程池的一个状态,这个肯定不会OOM。所以看后面这个表达式时干什么的
workQueue.offer(command)。command这个就是我们要执行的任务,workQueue看名字就知道这个是工作队列,其实就是我们那个阻塞队列。
在这里插入图片描述看一下workQueue的定义。
在这里插入图片描述我们跟踪一下workQueue的offer方法,发现有好几个实现,到底调用的是哪个?当然是LinkedBlockingQueue。
为什么?因为我们在创建newFixedThreadPoold的时候传入就是LinkedBlockingQueue
在这里插入图片描述那么我们这时候就可以去看一下LinkedBlockingQueue的offer方法。其实你在看异常的时候已经帮我们定位到了LinkedBlockingQueue的416行。
在这里插入图片描述
简单分析一下这个代码:

  • 411行 如果我的任务为null的话直接抛出空指针
  • 412行 获取当前队列的元素数
  • 413行 判断当前元素数是否等于队列的容量。如果等队列的容量直接返回false。
    而我们在通过Executors 来创建newFixedThreadPoold队列的时候并没有指定容量。所以会继续往下走
  • 415行 这里就不看它是做什么的了
  • 416行 这里就是就是new 了一个node节点,然后把我们的任务放进去
    停!!!到这里就结束了,OOM了,回想一下我写的代码,线程执行任务的速度要远远小于任务创建的速度,因为我任务里sleep了10000毫秒。而我们的队列又没有容量的限制,我一直在向LinkedBlockingQueue里放任务,它就会一直new node直到没有空间分配发生OOM

到这里,大家应该明白为什么阿里为什么要禁用 Executors 创建线程池?不是人道的毁灭,也不是道德的沦丧,而是因为Executors 创建的线程池会 有OOM的风险。
这时候就会有人问了,那我怎么去创建线程池?

创建线程池正确的姿势

到底怎么来创建线程池呢?其实也很简单,我基于之前的代码简单改造一下:
在这里插入图片描述我们直接通过ThreadPoolExecutor来创建线程池,创建了核心线程数为2,最大线程数为5,阻塞队列容量50000。我这里只是简单的演示,没有自定义线程工厂和拒绝策略,默认的拒绝策略是饱和之后报异常,可以给大家看一下:
在这里插入图片描述

总结一下,最好不要使用Executors 来创建线程池,一旦碰到执行的任务非常耗时,那么就有OOM的风险。快去检查一下自己项目里是否存在这样的风险吧。

猜你喜欢

转载自blog.csdn.net/u010994966/article/details/103123927