被开发者抛弃的 Executors,错在哪儿?

一. 序

在 Java 领域内,我们使用多线程的方式来实现并发编程。而线程本身是操作系统的一个概念,虽然不同的语言对线程都进行了一些封装,但是最终都是调用到操作系统中去创建和调度线程。

既然线程是一项重要的系统资源,为了更合理的利用此资源,我们会使用池化技术来优化线程的创建和销毁,这就是线程池

在我们学习并发编程的时候,线程可以利用 Thread 来创建并通过 start()来启动一个线,但在成熟的项目中,基本上是不允许这样操作线程的,都需要通过线程池去收敛线程的使用,所以线程池是必须的。

Java 的线程池可以通过 ThreadPoolExecutor 来构造,在其中提供非常完备的构造方法,可以根据我们的业务需求灵活的构造线程池。同时 Java 还提供了一个 Executors,它内部提供了很多包装的方法,利用它可以帮我们快速的构建线程池。

原本 Executors 的目的就是为了让我们更方便的使用线程池,但是《阿里巴巴Java开发手册》也明确指出,直接使用 Executors 的缺陷。

手册中提到强制不允许使用 Executors 去创建线程池,而是应该使用退化到最原始的 ThreadPoolExecutor 的方式。

日常开发中,应该收紧对线程池的创建,由开发人员明确线程池的运行规则,以此来尽量规避其资源耗尽的风险。

线程池是个好东西,但是怎么创建是一个问题。

二. Executors 怎么了?

2.1 不被允许的 Executors

不应该使用 Executors 的原因,其实《阿里巴巴Java开发手册》里已经写明了,当需要处理大量任务的时候,可能会出现 OOM 异常,但它们出现 OOM 的原因并不一样。

ThreadPoolExecutor 的构造方法中,提供了很多参数的配置,其中与 Executors 出现 OOM 相关的就有 2 个:核心线程数等待队列

先来看看 FixedThreadPool 和 SingleThreadPool 出现 OOM 的原因。

它们的问题在于等待队列使用了 LinkedBlockingQueue 这个以链表实现的无界队列(最大长度是 Integer.MAX_VALUE),最终导致堆积了大量等待处理的任务,从而导致频繁的 GC,最终触发 OOM。

java.lang.OutOfMemoryError: GC overhead limit exceeded

再来看看 CachedThreadPool 出现 OOM 的原因。

它的问题在于核心线程数设置为了 Integer.MAX_VALUE,并且等待队列是一个 SynchronousQueue。

SynchronousQueue 是一个没有数据缓冲的阻塞队列,它极易被阻塞。在等待队列被阻塞的时候,如果线程数量还没有达到核心线程数限制的数量时,线程池的策略是创建新的线程来处理新的任务。

也就是说,是核心线程数和等待队列 SynchronousQueue 合力造成了线程会跟随任务不断的被创建,直到触发 OOM。

java.lang.OutOfMemoryError: pthread_creat (1040KB stack) failed: Try again

ScheduledThreadPool 的等待队列使用的是 DelayedWorkQueue,原理也是类似的,最终会导致创建大量的线程而抛出 OOM。

线程是一种系统资源,本身创建就会带来内存开销,同时操作系统对单进程可创建的线程数也是有限制的。

在 Android 中,每个线程初始化都需要 mmap 一定的堆内存,在默认的情况下,初始化一个线程大约需要 mmap 1MB 左右的内存空间。同时系统本身也会对每个进程可创建的线程数,做一定的限制,这个限制在 /proc/pid/limits 中,不同的厂商对这个限制也有所不同,当超出限制时,哪怕堆上还有可用内存,依然会抛出 OOM。

2.2 Executors 错在哪儿了?

Executors 会在任务过多的时候,导致资源耗尽而触发 OOM,这是它带来的危害。

Executors 最大的问题,在于没有边界。

在系统环境良好,任务不多的时候 Executors 创建的线程池,都是可以正常工作的。

但是一旦有重压,我们就无法预知什么时候会出现问题,这就是没有边界,没有边界就意味着不可控。

我们很难去信任一段不可控的代码,它什么时候出现问题,完全是不可预知的,这才是 Executors 最大的问题。

除此之外,Executors 封装了太多线程池的细节,本身也不建议使用。例如通常我们需要給线程池创建的线程,起一个有意义的名称,方便在出现异常的时候排查问题;再例如对于线程池的拒绝策略,我们需要深思熟虑的定义,是直接抛弃还是持久化下来延迟处理。

去思考一个线程池的不同参数带来的策略细节,才是使用线程池的一个良好的开发习惯。

三. 小结时刻

本文我们聊了关于创建线程池,使用 Executors 创建的线程池会有 OOM 的风险,应该使用 ThreadPoolExecutor 去创建线程池。通过思考业务来明确配置线程池不同的参数,例如线程池、等待队列、拒绝策略等等。

在这分享一份整理了2个月的Android进阶面试解析笔记文档,包括了知识点笔记和高频面试问题解析及部分知识点视频讲解给大家!为了不影响阅读,在这以图片展示部分内容于目录截图,有需要的朋友麻烦点赞后点击下面在线链接获取免费领取方式吧!
阿里P6P7【安卓】进阶资料分享+加薪跳槽必备面试题

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值