首页 > 其他分享 >为什么不建议通过Executors构建线程池

为什么不建议通过Executors构建线程池

时间:2024-12-26 17:23:08浏览次数:5  
标签:线程 java Executors LinkedBlockingQueue 构建 new ThreadPoolExecutor

Executors类看起来功能还是比较强大的,又用到了工厂模式、又有比较强的扩展性,重要的是用起来还比较方便,如:

ExecutorService executor = Executors.newFixedThreadPool(nThreads) ;

即可创建一个固定大小的线程池。

但是为什么在阿里巴巴Java开发手册中也明确指出,不允许使用Executors创建线程池呢

Executors存在什么问题

在阿里巴巴Java开发手册中提到,使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出),但是并没有说明为什么,那么接下来我们就来看一下到底为什么不允许使用Executors?

我们先来一个简单的例子,模拟一下使用Executors导致OOM的情况。

/**
 * @author Hollis
 */
public class ExecutorsDemo {
    private static ExecutorService executor = Executors.newFixedThreadPool(15);
    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(new SubThread());
        }
    }
}

class SubThread implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            //do nothing
        }
    }
}

通过指定JVM参数:-Xmx8m -Xms8m 运行以上代码,会抛出OOM:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
    at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)

以上代码指出,ExecutorsDemo.java的第16行,就是代码中的executor.execute(new SubThread());

Executors为什么存在缺陷

通过上面的例子,我们知道了Executors创建的线程池存在OOM的风险,那么到底是什么原因导致的呢?我们需要深入Executors的源码来分析一下。

其实,在上面的报错信息中,我们是可以看出蛛丝马迹的,在以上的代码中其实已经说了,真正的导致OOM的其实是LinkedBlockingQueue.offer方法。

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
    at com.hollis.ExecutorsDemo.main(ExecutorsDemo.java:16)

如果读者翻看代码的话,也可以发现,其实底层确实是通过LinkedBlockingQueue实现的:

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

如果读者对Java中的阻塞队列有所了解的话,看到这里或许就能够明白原因了。

Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueueLinkedBlockingQueue

ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE

这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE

newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

上面提到的问题主要体现在newFixedThreadPoolnewSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPoolnewScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。

扩展知识

如何正确创建线程池

避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛`java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。

除了自己定义ThreadPoolExecutor外。还有其他方法。这个时候第一时间就应该想到开源类库,如apache和guava等。

作者推荐使用guava提供的ThreadFactoryBuilder来创建线程池。

public class ExecutorsDemo {

    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}

通过上述方式创建线程时,不仅可以避免OOM的问题,还可以自定义线程名称,更加方便的出错的时候溯源

标签:线程,java,Executors,LinkedBlockingQueue,构建,new,ThreadPoolExecutor
From: https://www.cnblogs.com/tzzp/p/18633640

相关文章

  • 更快、更灵活、场景更丰富,云效镜像构建能力升级啦!
    作者:崔力强在之前使用云效进行镜像构建时,你可能会遇到如下问题:只能对一个Registy进行login,如果FROM的image和push的imageregistry不相同,则无法实现。镜像构建缓存基于oss实现,有5G的大小限制,如果缓存过大,就无法使用。且每次全量上传缓存,耗时较长。为了解决......
  • 更快、更灵活、场景更丰富,云效镜像构建能力升级啦!
    作者:崔力强在之前使用云效进行镜像构建时,你可能会遇到如下问题:只能对一个Registy进行login,如果FROM的image和push的imageregistry不相同,则无法实现。镜像构建缓存基于oss实现,有5G的大小限制,如果缓存过大,就无法使用。且每次全量上传缓存,耗时较长。为了解决......
  • 在 Docker 中部署 Jenkins,并完成项目的构建和发布
    前言Jenkins的主要作用是帮助你,把需要在本地机器完成的Maven构建、Docker镜像发布、云服务器部署等系列动作全部集成在一个服务下。简化你的构建部署操作过程,因为Jenkins也被称为CI&CD(持续集成&持续部署)工具。提供超过1000个插件(Maven、Git、NodeJs)来支持构......
  • 数字孪生场景构建好处的详细阐述
    数字孪生场景构建是指利用物理模型、传感器更新、运行历史等数据,集成多学科、多物理量、多尺度、多概率的仿真过程,在虚拟空间中创建与现实世界中的物理对象或场景相对应的数字版“克隆体”,并对其进行全生命周期的映射和管理。这一技术带来了诸多好处,以下是对这些好处的详细阐述......
  • 复现线程池引发的生产环境BUG
    引言随着多线程和并发处理需求的增加,线程池成为了提升系统性能的重要工具。Java提供了强大的ThreadPoolExecutor类,能够高效地管理线程池,减少线程创建和销毁的开销。然而,当线程池达到其最大容量时,如何优雅地处理被拒绝的任务就成为了一个关键问题。本文将深入探讨Java......
  • 构建哈夫曼树
    构建哈夫曼树哈夫曼树(HuffmanTree),又称最优二叉树,是一种带权路径长度最短的二叉树,常用于数据压缩领域中的编码算法——哈夫曼编码。哈夫曼树是一种特殊的二叉树,其构造过程需要频繁地找到频率最小的两个节点并进行合并。这个过程可以通过最小堆来高效地实现。**堆(Heap)**是......
  • 49、Python入门 Python与AJAX:构建高效Web交互体验
             在现代Web开发中,Python作为后端语言以其简洁高效和丰富的库支持而广受欢迎,而AJAX(AsynchronousJavaScriptandXML)技术则为前端与后端的交互带来了革命性的变化。二者的结合能够构建出高效、流畅且具有卓越用户体验的Web应用。 一、AJAX技术概述AJAX不是......
  • 2. 线程
    这里所说的线程指程序执行过程中的一个线程实体。JVM允许一个应用并发执行多个线程。JVM中的Java线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java线程结束,原生线程随之被回......
  • Java面试要点99 - Java线程池的关闭过程
    文章目录引言一、线程池的关闭方式1.1shutdown方法1.2shutdownNow方法二、关闭过程中的状态转换2.1线程池状态监控2.2优雅关闭的实现三、任务处理与异常处理3.1关闭时的任务处理3.2关闭过程中的异常处理总结引言线程池的关闭是Java并发编程中的重要环节,......
  • Java面试要点98 - Java中线程池的任务提交过程
    文章目录引言一、任务提交方式1.1execute方法1.2submit方法二、任务执行流程2.1核心流程分析2.2任务状态转换三、任务队列处理3.1队列类型选择3.2队列满时的处理四、异常处理4.1提交时异常处理4.2执行时异常处理总结引言在Java并发编程中,了解线程池的......