计算机系统应用教程网站

网站首页 > 技术文章 正文

正确使用线程池的姿势(一) 线程池使用注意事项

btikc 2024-10-22 10:31:32 技术文章 6 ℃ 0 评论

线程作为一种计算机珍贵的资源,在实际的开发中,我们通常会采用池化技术(线程池)来缓存线程对象。

下面我们通过三个问题来看看,使用线程池应该注意的问题。

线程池应该手动声明

在java的JDK中提供了Executors类可以快速地创建一些线程池,具体代码如下

// 创建工作线程数固定的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
    }
// 创建一个线程可复用的线程池
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
    }
// 创建只有一个工作线程的线程池
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
    }
// 创建定时/延时工作线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

在《阿里巴巴java开发手册》中有着明确的强制性的要求:【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。最典型的就是FixedThreadPoolCachedThreadPool,可能会因为资源耗尽,进而导致OOM

  • FixedThreadPool是如何造成OOM

我们先编写一段测试代码,代码大致要执行的逻辑是:初始化一个工作线程数为2的FixedThreadPool,循环1亿次向线程池提交任务,每个线程内执行的逻辑是,创建一个比较大的字符串,然后再休眠30min。执行该代码一段时间后,可以看到打印出来如下的错误信息。

    public static void oom1() throws InterruptedException {
        ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
        //打印线程池的信息,稍后我会解释这段代码
        for (int i = 0; i < 100000000; i++) {
            threadPool.execute(() -> {
                String payload = IntStream.range(1, 1000000)
                        .mapToObj(k -> "a")
                        .collect(Collectors.joining("")) + UUID.randomUUID().toString();
                try {
                    TimeUnit.MINUTES.sleep(30);
                } catch (InterruptedException e) {
                }
                log.info(payload);
            });
        }
        threadPool.shutdown();
        threadPool.awaitTermination(1, TimeUnit.HOURS);
    }

PS:如果大家不好模拟出来OMM,可以设置jvm的最大堆。

问题剖析:打开newFixedThreadPool的源码,我们不难发现问题的根源“线程池的任务队列创建了一个LinkedBlockingQueue,而LinkedBlockingQueue的默认构造方法是创建一个长度为Integer.MAX_VALUE的队列,实际使用中可以认为该队列是无界队列,虽然线程池的工作线程数是固定的,但如果快速提交了大量任务且这些任务的执行都比较耗时,未执行的任务会存储在队列中,以致撑爆内存导致OOM

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

 public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
  • CachedThreadPool是如何造成OOM

我们稍微修改一下上面的代码,将newFixedThreadPool改为newCachedThreadPool,代码如下

 public static void oom2() throws InterruptedException {
        ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();
        //打印线程池的信息,稍后我会解释这段代码
        for (int i = 0; i < 100000000; i++) {
            threadPool.execute(() -> {
                String payload = IntStream.range(1, 1000000)
                        .mapToObj(k -> "a")
                        .collect(Collectors.joining("")) + UUID.randomUUID().toString();
                try {
                    TimeUnit.MINUTES.sleep(30);
                } catch (InterruptedException e) {
                }
                log.info(payload);
            });
        }
        threadPool.shutdown();
        threadPool.awaitTermination(1, TimeUnit.HOURS);
    }

程序运行后,很快就打印出了如上的错误。

问题剖析:打开newCachedThreadPool的源码,我们不难发现问题的根源“线程池的工作线程数是Integer.MAX_VALUE,而其任务队列SynchronousQueue是一个没有存储空间的阻塞队列,因此实际使用中当接收到一个任务后,就必须有一个线程来处理该任务,如果当前的线程池没有空闲的线程,就会创建一个新的线程”,此时由于我们的任务执行比较耗时,提交大量任务就需要创建大量得线程,我们都知道创建线程是需要分配一定得内存空间来作为线程栈得,例如1MB。如果无限制的创建线程,必然会导致OOM。

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

  public SynchronousQueue() {
        this(false);
    }

因此,在实际的开发中我们是强制不要使用以上方法创建线程池。除了因为会造成OOM的原因外还有以下两点:

  1. 通常情况下,我们需要根据自己的业务场景、并发情况以及服务器的硬件配置等因素来综合评估线程池的核心参数,包含核心线程数、最大线程数、任务队列类型、线程回收策略以及任务拒绝策略,确保我们创建的线程池符合需求,一般都需要设置有界的任务队列和可控的工作线程数。
  2. 任何场景下,都应该为自定义的线程池指定有意义的名称,方便出现问题时,进行排查。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表