# 《并发设计模式》第36章-线程池模式-无法创建新的本地线程?

作者:冰河
星球:http://m6z.cn/6aeFbs (opens new window)
博客:https://binghe.gitcode.host (opens new window)
文章汇总:https://binghe.gitcode.host/md/all/all.html (opens new window)
源码获取地址:https://t.zsxq.com/0dhvFs5oR (opens new window)

沉淀,成长,突破,帮助他人,成就自我。

  • 本章难度:★★☆☆☆
  • 本章重点:初步了解线程池的应用场景,重点理解线程池模式的核心原理和应用,并能够结合自身项目实际场景思考如何将线程池模式灵活应用到自身实际项目中。

大家好,我是冰河~~

在使用线程池实现功能之前,一定要对每种线程池的实现原理和注意事项了然于胸,不然,就会出现本想使用线程池解决问题,却没想到适得其反,引发其他严重事故的问题。对线程池一定要知其然,更要知其所以然。

# 一、故事背景

小菜被告知生产环境社区电商系统的优惠券服务内存和CPU占用都非常高,服务器处于假死状态。于是,小菜将社区电商系统的优惠券服务拉取下来,发现程序中会为每条消息的推送都会创建一个线程来执行,他第一时间想到的就是使用线程池,但有想到推送消息的效率问题,或者说是尽可能快的将消息推送出去,小菜使用了代码Executors.newCachedThreadPool()来创建线程池,没想到这次服务器的CPU又爆了。在无奈的情况下,小菜决定还是请教老王,让老王为其分析下遇到的问题,老王也为小菜耐心的进行了讲解。

# 二、复现代码

为了更好的复现小菜遇到的问题,这里,我们模拟实现小菜使用线程池向用户手机APP推送消息。

源码详见:io.binghe.concurrent.design.thread.pool.wrong.MessageWrongThreadPoolTest。

public class MessageWrongThreadPoolTest {

    private static final ExecutorService THREAD_POOL = Executors.newCachedThreadPool();
    private static final long MESSAGE_COUNT = 1000;
    
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "执行关闭线程池的操作");
            THREAD_POOL.shutdown();
        },"shudown-hook-thread-"));

        MessageService messageService = new MessageServiceImpl();

        for (long i = 0; i < MESSAGE_COUNT; i++){
            THREAD_POOL.execute(() -> messageService.sendMessage("恭喜您获取一张50元的优惠券"));
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

大家可以自行运行上述代码,在测试的过程中,可以不断增加MESSAGE_COUNT的值,当达到一定程度时,看是否会抛出java.lang.OutOfMemoryError: unable to create new native thread异常,这个异常就是无法创建新的本地线程。

# 三、分析Executors类

在分析小菜遇到的CPU占用高的问题之前,我们先来分析下Executors类中创建线程池的方法,因为小菜本身就是使用Executors类中的newCachedThreadPool()方法来创建线程池的,在Executors类中,提供了大量创建线程池的方法,如下所示。

(1)Executors.newCachedThreadPool()方法

创建一个可缓存的线程池,如果线程池中存在的线程数量超过程序处理的需要,则线程池可以根据具体情况灵活回收空闲的线程。如果向线程池提交任务时,线程池中没有空闲的线程,则新建线程处理任务。

(2)Executors.newFixedThreadPool()方法

创建一个定长的线程池,可以控制线程池中线程的最大并发数。向线程池提交任务时,如果线程池中有空闲线程,则分配一个空闲线程执行任务。如果线程池中没有空闲线程,则将提交的任务放入阻塞队列中等待。

(3)Executors.newScheduledThreadPool()方法

创建一个支持定时、周期性执行任务的线程池。

(4)Executors.newSingleThreadExecutor()方法

创建内部只有一个线程的线程池,线程池内部使用一个唯一的线程执行任务,提交到线程池中的任务都会按照先到先处理的原则串行执行。

(5)Executors.newSingleThreadScheduledExecutor()方法

创建内部只要一个线程,并且支持定时、周期性执行任务的线程池。

(6)Executors.newWorkStealingPool()方法

从JDK1.8版本开始提供的方法,底层使用的是 ForkJoinPool 实现,创建一个拥有多个任务队列的线程池,可以实现任务的并行执行。

使用Executors.newFixedThreadPool()方法和Executors.newSingleThreadExecutor()方法创建线程池时,由于内部阻塞队列使用的是LinkedBlockingQueue的默认构造方法进行初始化,默认的LinkedBlockingQueue队列的长度时Integer.MAX_VALUE,使用Executors.newFixedThreadPool()方法和Executors.newSingleThreadExecutor()方法创建的线程池在高并发环境下容易发生内存泄露的问题。另外,在高并发环境下使用Executors.newCachedThreadPool()方法创建的线程池容易导致CPU占用100%的问题。

# 四、分析问题

复现小菜的代码并分析了Executors类后,我们再来分析下小菜遇到的问题。其实,本质上小菜遇到的问题还是使用了Executors.newCachedThreadPool()方法来创建线程池导致的,那为什么使用Executors.newCachedThreadPool()方法创建线程池,在大并发场景下就会出现无法创建本地线程的异常呢?

# 查看全文

加入冰河技术 (opens new window)知识星球,解锁完整技术文章与完整代码