被开发者抛弃的 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 去创建线程池。通过思考业务来明确配置线程池不同的参数,例如线程池、等待队列、拒绝策略等等。
今天就到这里,有任何问题,欢迎留言讨论。
本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!
公众号后台回复成长『成长』,将会得到我准备的学习资料。