由于不知道 Java 线程池的 bug, 某程序员叕被祭天
说说你对线程池的理解?
首先明确,池化的意义在于缓存,创建性能开销较大的对象,比如线程池、连接池、内存池。预先在池里创建一些对象,使用时直接取,用完就归还复用,使用策略调整池中缓存对象的数量。
Java 创建对象,仅是在 JVM 堆分块内存,但创建一个线程,却需调用 os 内核 API,然后 os 要为线程分配一系列资源,成本很高,所以线程是一个重量级对象,应避免频繁创建或销毁。既然这么麻烦,就要避免呀,所以要使用线程池!
一般池化资源,当你需要资源时,就调用申请线程方法申请资源,用完后调用释放线程方法释放资源。但 JDK 的线程池根本没有申请线程和释放线程的方法。
那到底该如何理解它的设计思想呢?其实线程池的设计,采用的是生产者-消费者模式:
线程池的使用方是生产者
线程池本身是消费者
以下简化代码即可显示线程池的基本原理:
JDK 线程池最核心的就是 ThreadPoolExecutor,看名字,它强调的是 Executor,并非一般的池化资源。
为什么都说要手动声明线程池?
虽然 JDK 的Executors
工具类提供的方法可快速创建线程池。
但阿里有话说:
弊端真的这么严重吗,newFixedThreadPool=OOM?
写段测试代码:
执行不久,出现 OOM
newFixedThreadPool
线程池的工作队列直接 new 了一个 LinkedBlockingQueue
但其默认构造器是一个
Integer.MAX_VALUE
长度的队列,所以很快 Q 满
使用newFixedThreadPool
可以固定工作线程数量,但任务队列几乎无界。若任务较多且执行较慢,队列就会快速积压,内存不够,易导致 OOM。
newCachedThreadPool 也等于 OOM?
可见 OOM 是因为无法创建线程,newCachedThreadPool 这种线程池的最大线程数是 Integer.MAX_VALUE,也可认为无上限,而其工作队列 SynchronousQueue 是一个没有存储空间的阻塞队列。
所以只要有请求到来,就必须找到一条工作线程处理,若当前无空闲线程就再创建一个新的。由于我们的任务需很长时间才能执行完成,大量任务进来后会创建大量线程。而线程是需要分配一定内存空间作为线程栈的,比如 1MB,因此无限创建线程必 OOM
所以使用线程池,请不要抱任何侥幸,以为只是处理轻量任务,不会造成队列积压或创建大量线程!比如某业务一旦接受到请求,就会调用外部服务,该外部服务接口正常 100ms 内会响应,现在 TPS 过百,CachedThreadPool 能稳定在占用 10 个左右线程情况下满足需求。可天有不测风云,该外部服务不可用了!而代码里调用该服务设置的超时又特别长, 比如 1min,1min 可能已经进成千上万请求,产生几千个任务,需几千个线程,没多久就因为无法再创建新线程,OOM!
所以阿里才不建议使用 Executors:
要结合实际并发情况,评估线程池核心参数,确保其工作行为符合预期,关键的也就是设置有界工作队列和数量可控的线程数
永远要为自定义的线程池设置有意义名称,以便排查问题
因为当出现线程数量暴增、死锁、CPU 负载高、线程执行异常等事故时,往往都需抓取线程栈。有意义的线程名称,就很重要。示例如下:
异常处理
通过 ThreadPoolExecutor#execute()提交任务时,若任务在执行的过程中出现运行时异常,会导致 执行任务的线程 终止。但要命的是,有时任务虽然异常了,但你却收不到任何通知,你还在开心摸鱼,以为任务都执行很正常。虽然线程池提供了很多用于异常处理的方法,但最稳妥和简单的方案还是捕获所有异常并具体处理:
线程池的线程管理
还好有谷歌,一般我们直接利用 guava 的 ThreadFactoryBuilder 实现线程池线程的自定义命名即可。
线程池的拒绝策略
线程池默认的拒绝策略会抛 RejectedExecutionException,这是个运行时异常,IDEA 不会强制捕获,所以我们也很容易忽略它。对于采用何种策略,具体要看任务的重要性:
若是一些不重要任务,可选择直接丢弃
重要任务,可采用降级,比如将任务信息插入 DB 或 MQ,启用一个专门用作补偿的线程池去补偿处理。所谓降级,也就是在服务无法正常提供功能的情况下,采取的补救措施。具体处理方式也看具体场景而不同。
当线程数大于核心线程数时,线程等待 keepAliveTime 后还是无任务需要处理,收缩线程到核心线程数了解这个策略,有助于我们根据实际的容量规划需求,为线程池设置合适的初始化参数。也可通过一些手段来改变这些默认工作行为,比如:
声明线程池后立即调用 prestartAllCoreThreads,启动所有核心线程
传 true 给 allowCoreThreadTimeOut,让线程池在空闲时同样回收核心线程
弹性伸缩的实现
线程池是先用 Q 存放来不及处理的任务,满后再扩容线程池。当 Q 设置很大时(那个 工具类),最大线程数这个参数就没啥意义了,因为队列很难满或到满时可能已 OOM,更没机会去扩容线程池了。是否能让线程池优先开启更多线程,而把 Q 当成后续方案?比如我们的任务执行很慢,需要 10s,若线程池可优先扩容到 5 个最大线程,那么这些任务最终都可以完成,而不会因为线程池扩容过晚导致慢任务来不及处理。
难题在于:
线程池在工作队列满时,会扩容线程池
重写队列的 offer,人为制造该队列满的条件
改变了队列机制,达到最大线程后势必要触发拒绝策略
实现一个自定义拒绝策略,这时再把任务真正插入队列
Tomcat 就实现了类似的“弹性”线程池。
务必确认清楚线程池本身是不是复用的。
某服务偶尔报警线程数过多,但过一会儿又会降下来,但应用的请求量却变化不大。
可以在线程数较高时抓取线程栈,发现内存中有上千个线程池,这肯定不正常!
但代码里也没看到声明了线程池,最后发现原来是业务代码调用了一个类库:
该类库竟然每次都创建一个新的线程池!
newCachedThreadPool 会在需要时创建必要数量的线程,业务代码的一次业务操作会向线程池提交多个慢任务,这样执行一次业务操作就会开启多个线程。如果业务操作并发量较大的话,的确有可能一下子开启几千个线程。
那为何监控中看到线程数量会下降,而不 OOM?
newCachedThreadPool 的核心线程数是 0,而 keepAliveTime 是 60s,所以 60s 后所有线程都可回收。
那这如何修复呢?
使用 static 字段存放线程池引用即可
线程池的意义在于复用,就意味着程序应该始终使用一个线程池吗?
不,具体场景具体分析。
比如一个 I/O 型任务,不断向线程池提交任务:向一个文件写入大量数据。线程池的线程基本一直处于忙碌状态,队列也基本满。而且由于是 CallerRunsPolicy 策略,所以当线程满队列满,任务会在提交任务的线程或调用 execute 方法的线程执行,所以不要认为提交到线程池的任务就一定会被异步处理。
毕竟,若使用 CallerRunsPolicy,就有可能异步任务变同步执行。使用 CallerRunsPolicy,当线程池饱和时,计算任务会在执行 Web 请求的 Tomcat 线程执行,这时就会进一步影响到其他同步处理的线程,甚至造成整个应用程序崩溃。
如何修正?
使用单独的线程池处理这种“I/O 型任务”,将线程数设置多一些!
所以千万不要盲目复用别人写的线程池!因为它不一定适合你的任务!
Java 8 的 parallel stream
可方便并行处理集合中的元素,共享同一 ForkJoinPool,默认并行度是 CPU 核数-1。对于 CPU 绑定的任务,使用这样的配置较合适,但若集合操作涉及同步 I/O 操作(比如数据库操作、外部服务调用),建议自定义一个 ForkJoinPool(或普通线程池)。
最后声明一点:提交到相同线程池中的任务,一定要是相互独立的,最好不要有依赖关系!
参考
《阿里巴巴 Java 开发手册》
版权声明: 本文为 InfoQ 作者【JavaEdge】的原创文章。
原文链接:【http://xie.infoq.cn/article/181444b60d75738a3b5342601】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论