一、被开序 在 Java 领域内,抛弃我们使用多线程的错儿方式来实现并发编程。而线程本身是被开操作系统的一个概念,虽然不同的抛弃语言对线程都进行了一些封装,但是错儿最终都是调用到操作系统中去创建和调度线程。 既然线程是被开一项重要的系统资源,为了更合理的抛弃利用此资源,我们会使用池化技术来优化线程的错儿创建和销毁,这就是被开线程池。 在我们学习并发编程的抛弃时候,线程可以利用 Thread 来创建并通过 start() 来启动一个线,错儿但在成熟的被开项目中,基本上是抛弃不允许这样操作线程的,都需要通过线程池去收敛线程的错儿使用,所以线程池是必须的。云服务器提供商 Java 的线程池可以通过 ThreadPoolExecutor 来构造,在其中提供非常完备的构造方法,可以根据我们的业务需求灵活的构造线程池。同时 Java 还提供了一个 Executors,它内部提供了很多包装的方法,利用它可以帮我们快速的构建线程池。 原本 Executors 的目的就是为了让我们更方便的使用线程池,但是《阿里巴巴Java开发手册》也明确指出,直接使用 Executors 的缺陷。 手册中提到强制不允许使用 Executors 去创建线程池,而是应该使用退化到最原始的 ThreadPoolExecutor 的方式。 日常开发中,应该收紧对线程池的创建,由开发人员明确线程池的运行规则,以此来尽量规避其资源耗尽的风险。源码下载 线程池是个好东西,但是怎么创建是一个问题。 二、Executors 怎么了? 1. 不被允许的 Executors 不应该使用 Executors 的原因,其实《阿里巴巴Java开发手册》里已经写明了,当需要处理大量任务的时候,可能会出现 OOM 异常,但它们出现 OOM 的原因并不一样。 ThreadPoolExecutor 的构造方法中,提供了很多参数的配置,其中与 Executors 出现 OOM 相关的就有 2 个:核心线程数和等待队列。 先来看看 FixedThreadPool 和 SingleThreadPool 出现 OOM 的原因。 它们的问题在于等待队列使用了 LinkedBlockingQueue 这个以链表实现的无界队列(最大长度是 Integer.MAX_VALUE),最终导致堆积了大量等待处理的任务,从而导致频繁的 GC,最终触发 OOM。 再来看看 CachedThreadPool 出现 OOM 的香港云服务器原因。 它的问题在于核心线程数设置为了 Integer.MAX_VALUE,并且等待队列是一个 SynchronousQueue。 SynchronousQueue 是一个没有数据缓冲的阻塞队列,它极易被阻塞。在等待队列被阻塞的时候,如果线程数量还没有达到核心线程数限制的数量时,线程池的策略是创建新的线程来处理新的任务。 也就是说,是核心线程数和等待队列 SynchronousQueue 合力造成了线程会跟随任务不断的被创建,直到触发 OOM。 ScheduledThreadPool 的等待队列使用的是 DelayedWorkQueue,原理也是类似的,最终会导致创建大量的线程而抛出 OOM。 线程是一种系统资源,本身创建就会带来内存开销,同时操作系统对单进程可创建的线程数也是有限制的。 在 Android 中,每个线程初始化都需要 mmap 一定的堆内存,在默认的情况下,初始化一个线程大约需要 mmap 1MB 左右的内存空间。同时系统本身也会对每个进程可创建的线程数,做一定的限制,这个限制在 /proc/pid/limits 中,不同的厂商对这个限制也有所不同,当超出限制时,哪怕堆上还有可用内存,依然会抛出 OOM。 2. Executors 错在哪儿了? Executors 会在任务过多的时候,导致资源耗尽而触发 OOM,这是它带来的危害。 Executors 最大的问题,在于没有边界。 在系统环境良好,任务不多的时候 Executors 创建的线程池,都是可以正常工作的。 但是一旦有重压,我们就无法预知什么时候会出现问题,这就是没有边界,没有边界就意味着不可控。 我们很难去信任一段不可控的代码,它什么时候出现问题,完全是不可预知的,这才是 Executors 最大的问题。 除此之外,Executors 封装了太多线程池的细节,本身也不建议使用。例如通常我们需要給线程池创建的线程,起一个有意义的名称,方便在出现异常的时候排查问题;再例如对与线程池的拒绝策略,我们需要深思熟虑的定义,是直接抛弃还是持久化下来延迟处理。 去思考一个线程池的不同参数带来的策略细节,才是使用线程池的一个良好的开发习惯。 三. 小结时刻 本文我们聊了关于创建线程池,使用 Executors 创建的线程池会有 OOM 的风险,应该使用 ThreadPoolExecutor 去创建线程池。通过思考业务来明确配置线程池不同的参数,例如线程池、等待队列、拒绝策略等等。