Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
立即订阅
3216 人已学习
课程目录
已更新 5 讲 / 共 37 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 业务代码真的会有这么多坑?
免费
代码篇 (4讲)
01 | 使用了并发工具类库,线程安全就高枕无忧了吗?
02 | 代码加锁:不要让“锁”事成为烦心事
03 | 线程池:业务代码最常用也最容易犯错的组件
04 | 连接池:别让连接池帮了倒忙
Java 业务开发常见错误 100 例
登录|注册

03 | 线程池:业务代码最常用也最容易犯错的组件

朱晔 2020-03-12
你好,我是朱晔。今天,我来讲讲使用线程池需要注意的一些问题。
在程序中,我们会用各种池化技术来缓存创建昂贵的对象,比如线程池、连接池、内存池。一般是预先创建一些对象放入池中,使用的时候直接取出使用,用完归还以便复用,还会通过一定的策略调整池中缓存对象的数量,实现池的动态伸缩。
由于线程的创建比较昂贵,随意、没有控制地创建大量线程会造成性能问题,因此短平快的任务一般考虑使用线程池来处理,而不是直接创建线程。
今天,我们就针对线程池这个话题展开讨论,通过三个生产事故,来看看使用线程池应该注意些什么。

线程池的声明需要手动进行

Java 中的 Executors 类定义了一些快捷的工具方法,来帮助我们快速创建线程池。《阿里巴巴 Java 开发手册》中提到,禁止使用这些方法来创建线程池,而应该手动 new ThreadPoolExecutor 来创建线程池。这一条规则的背后,是大量血淋淋的生产事故,最典型的就是 newFixedThreadPool 和 newCachedThreadPool,可能因为资源耗尽导致 OOM 问题。
首先,我们来看一下 newFixedThreadPool 为什么可能会出现 OOM 的问题。
我们写一段测试代码,来初始化一个单线程的 FixedThreadPool,循环 1 亿次向线程池提交任务,每个任务都会创建一个比较大的字符串然后休眠一小时:
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java 业务开发常见错误 100 例》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(14)

  • Darren
    第一个问题的来了,请老师指点:
    https://github.com/y645194203/geektime-java-100/blob/master/ExtremeThreadPoolExecutor.java

    里面自定义了一个extremeOffer方法,因为不是BlockQUeue接口的方法,所以在执行拒绝策略后,真正加入阻塞队列的时候强转了一些,感觉不是很好,有没有更好的处理方法,请老师指点下。


    加上之前回答的第二个问题答案:
    不会被回收,会OOM,即使是自定义线程池,核心线程是不会回收的,每次需要10个线程,刚好是核心线程数,因此每次请求都会创建10个核心线程数的线程池,请求次数多了后,很快就回OOM。
    Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

    作者回复: 直接用put即可,可以参考这里的回复:
    https://stackoverflow.com/questions/19528304/how-to-get-the-threadpoolexecutor-to-increase-threads-to-max-before-queueing
    不过要考虑选择丢数据还是阻塞

    其实,实现自己的RejectedExecutionHandler耦合自己的Queue也无可厚非。Tomcat也是这样的,其实现参考这里:https://github.com/apache/tomcat/blob/a801409b37294c3f3dd5590453fb9580d7e33af2/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java

    2020-03-12
    4
    3
  • 陈天柱
    首先赞一下老师的排查问题的思路!!!然后针对第二个问题,我觉得不会被回收且很快就会OOM了,因为每次请求都新建线程池,每个线程池的核心数都是10, 虽然自定义线程池设置2秒回收,但是没超过线程池核心数10是不会被回收的, 不间断的请求过来导致创建大量线程,最终OOM

    作者回复: 👍🏻

    2020-03-12
    2
  • G小调
    文章非常棒 通过真实案例讲解 透过现象看本质
    2020-03-12
    2
  • Darren
    先回答第二个问题吧,第一个等天亮了,试一试
    不会被回收,会OOM,即使是自定义线程池,核心线程是不会回收的,每次需要10个线程,刚好是核心线程数,因此每次请求都会创建10个核心线程数的线程池,请求次数多了后,很快就回OOM。
    Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

    作者回复: 👍🏻

    2020-03-12
    2
  • 小美
    第二个问题大家都说核心线程数不会被回收,但是方法执行完线程池的引用已经引用不到了吧,线程池对象会被垃圾回收吧,垃圾回收时核心线程怎么办呢

    作者回复: ThreadPoolExecutor回收不了,可以看看其源码,工作线程Worker是内部类,只要它活着,换句话说线程在跑,就会阻止ThreadPoolExecutor回收,所以其实ThreadPoolExecutor是无法回收的,并不能认为ThreadPoolExecutor没有引用就能回收

    2020-03-14
  • mgs2002
    老师,我还有个问题, 我在本机做了一下默认线程池(CompletableFuture)和自定义线程池的调用时间对比,测试了好多次每次都是默认线程池的时间快,任务越多自定义线程池的时间就越慢,默认的相对稳定,不懂怎么回事了。。
    这是我的测试结果,default是默认的,custom是自定义的
     When 1 tasks => future default: 11,future custom: 4
     When 3 tasks => future default: 1,future custom: 2
     When 4 tasks => future default: 1,future custom: 2
     When 5 tasks => future default: 1,future custom: 6
     When 6 tasks => future default: 1,future custom: 7
     When 8 tasks => future default: 1,future custom: 5
     When 9 tasks => future default: 0,future custom: 5
     When 14 tasks => future default: 1,future custom: 5
     When 20 tasks => future default: 2,future custom: 9
     When 21 tasks => future default: 1,future custom: 7
     When 34 tasks => future default: 2,future custom: 4
     When 54 tasks => future default: 1,future custom: 2
     When 104 tasks => future default: 2,future custom: 2
     When 154 tasks => future default: 3,future custom: 3
     When 204 tasks => future default: 4,future custom: 47
     When 304 tasks => future default: 5,future custom: 481
     When 604 tasks => future default: 9,future custom: 488
     When 1004 tasks => future default: 18,future custom: 783
     When 2004 tasks => future default: 41,future custom: 1573
     When 3004 tasks => future default: 64,future custom: 2628
    自定义连接池设定参数:
        corePoolSize:200 maximumPoolSize:200 keepAliveTime:1 unit:HOUR workQueue:200
        默认拒绝策略
    测试代码:
     @PostMapping("/testPool")
        public Result list(@Valid @RequestBody SchoolRequest schoolRequest){
            Arrays.asList(-3, -1, 0, 1, 2, 4, 5, 10, 16, 17, 30, 50, 100, 150, 200, 300,600,1000,2000,3000).forEach(offset -> {
                int jobNum = 4 + offset;
                log.info(String.format("When %s tasks => future default: %s,future custom: %s",jobNum, testCompletableFutureDefaultExecutor(jobNum,schoolRequest),testCompletableFutureCustomExecutor(jobNum,schoolRequest)));
            });
            return Result.successInstance();
        }

    作者回复: 这段代码不是很能理解意思,testCompletableFutureDefaultExecutor是什么?最好给出源码链接

    2020-03-14
  • 汝林外史
    1. 既然选择先扩容线程池再加入队列,那为什么不干脆把核心线程数设置大一些,然后核心线程数可回收这种策略呢?
    2. 复用线程池,任务很慢,主线程get结果的时候不会导致主线程卡死的状态吗?不是也提倡不同的任务用不同的线程池,那复用与不复用的边界在哪里呢?是要根据也无需求自己评估吗?

    作者回复: 1. 你说的这种策略,此文也有提到: https://stackoverflow.com/questions/19528304/how-to-get-the-threadpoolexecutor-to-increase-threads-to-max-before-queueing
    其实,我们希望的是尽量确保有足够多的线程能处理任务,但是又不闲置过多线程,或临时创建过多线程,换句话说让线程的创建和回收不要太频繁。选择哪个策略要根据任务的性质和压力的流量形态来决定。

    2. 这里我说的复用线程池是指不每次都创建线程池,线程池必须复用而不是按需创建,但是不推荐一味混用一个线程池。对于选择是否混用线程池,至少对于频+快的任务和少+慢的任务应该分开,还是要根据实际任务的性质来选择。

    2020-03-14
  • 张少华
    挺厉害
    2020-03-13
  • mgs2002
    学到了,我现在代码还是用的CompletableFuture默认线程池。。。有个问题请教老师,我有两个项目,里面有很多数据统计的地方使用到了线程池,这种是属于CPU绑定类型的吧(通过数据库来查询统计),还有我想分别给两个项目设置不同的线程池可行吗

    作者回复: 通过数据库来查询统计就是IO操作,既然项目都不同了,那么就不是一个进程了,我们说的线程池隔离是针对一个进程的

    2020-03-13
    1
  • HW
    mysqld: Table 'mysql.plugin' doesn't exist
    [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choos
    [Warning] Failed to open optimizer cost constant tables
    [ERROR] Fatal error: Can't open and lock privilege tables: Table 'mysql.user' doesn't exist
    [ERROR] Fatal error: Failed to initialize ACL/grant/time zones structures or failed to remove temporary table files.
    [ERROR] Aborting
    mysqld: Shutdown complete

    ---
    老师您好,docker-compose up 在启动mysql时遇到了这个异常,看网上是说没有初始化mysql,麻烦请问下,是需要进到docker容器里面去进行初始化或配置吗?(刚刚接触docker,还望指点一下,谢谢)

    作者回复: 你是什么os呢?我是macos没有遇到过这个问题,尝试先重启docker service,或OS。docker-compose是方便大家快速启动依赖,不是必须的,实在不行你可以自己手动安装mysql,都是一样的

    2020-03-13
    2
  • 汝林外史
    老师的文章真的是看得很爽。问题如下:
    1.激进的都适应什么场合呢?先扩容线程池再加入任务队列,也可能队列满了还来任务,还是要再拒绝。
    2.因为核心线程不会回收,所以会OOM。可以设置allowCoreThreadTimeOut参数让核心线程也可以回收。 另外文中的ThreadPoolHelper是用来复用线程池的,但是提交的都是慢任务,很多地方都提交的话不会导致线程池始终处于满的状态,导致后来的任务始终得不到执行吗?

    作者回复: 1、可以想想tomcat为什么觉得这样激进的线程更适合。其实.NET的线程池就是这样的弹性线程池,只不过创建新的线程还会有一定的思考时间,延迟新线程的创建,更智能。
    2、其实和任务慢不慢没有太大关系,即使任务不慢,不复用也是有问题的。

    2020-03-12
  • pedro
    总结一下今天学到的;1、线程池的OOM问题,可能是队列满造成的,也可能是线程太多造成的,至于后面的那个2000个线程池太多,大概是这辈子都不会遇到这种错误吧😂。2、线程池的策略问题,优先根据场景来选择合适的参数来新建线程池,若还是无法满足,可自定义线程池,总之一切以实际为准。
    2020-03-12
  • 🐾
    OOM,全称“Out Of Memory”,中文意思是“内存用尽”。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也没有空间可回收时,就会抛出这个 Error。
    2020-03-12
  • JavaGuide
    很棒!
    2020-03-12
收起评论
14
返回
顶部