线程池
线程池相关问题
什么是线程池
顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
为什么要用线程池
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。
线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如何创建线程池
方式一:通过ThreadPoolExcutor
构造函数来创建(推荐)
从图中可知,ThreadPoolExecutor
的构造参数有
corePoolSize
、maximumPoolSize
、keepAliveTime
、unit
、workQueue
、threadFactory
、handler
。
corePoolSize:
核心线程池的大小。这是线程池中保持活动的最小线程数。即使线程处于空闲状态,核心线程也不会被终止。如果线程池中的线程数小于这个值,则线程池会创建新的线程。
maximumPoolSize:
线程池能够容纳的最大线程数。如果线程池中的线程数超过这个值,则任务将被阻塞。如果线程池中的线程在一段时间内没有执行任务,它们将被终止。
keepAliveTime:
非核心线程(数量超过corePoolSize
的线程)的空闲时间。如果一个非核心线程空闲时间超过这个值,且线程数超过corePoolSize
,则这个线程会被终止。
unit:
keepAliveTime
的时间单位。可以是TimeUnit
的枚举值,如TimeUnit.SECONDS
,TimeUnit.MILLISECONDS
等。
workQueue:
任务队列。它存储提交但尚未执行的任务。这个参数是可选的,如果没有提供,则使用无界队列。
threadFactory:
用于创建新线程的工厂。它用于定义新线程的名称和其它一些属性。
handler:
当任务队列已满并且核心线程数已满时,这个处理器将处理无法执行的任务。它定义了如何拒绝多余的任务。
方式二:通过
Executor
框架的工具类 Executors
来创建
我们可以创建多种类型的 ThreadPoolExecutor
:
FixedThreadPool
:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。SingleThreadExecutor
: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。CachedThreadPool
: 该方法返回一个可根据实际情况调整线程数量的线程池。初始大小为 0。当有新任务提交时,如果当前线程池中没有线程可用,它会创建一个新的线程来处理该任务。如果在一段时间内(默认为 60 秒)没有新任务提交,核心线程会超时并被销毁,从而缩小线程池的大小。ScheduledThreadPool
:该方法返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
对应 Executors
工具类中的方法如图所示:
为什么不推荐使用内置线程池
在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
为什么呢?
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用
Executors
去创建,而是通过 ThreadPoolExecutor
构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors
返回线程池对象的弊端如下:
FixedThreadPool
和SingleThreadExecutor
:使用的是无界的LinkedBlockingQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。CachedThreadPool
:使用的是同步队列SynchronousQueue
, 允许创建的线程数量为Integer.MAX_VALUE
,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。ScheduledThreadPool
和SingleThreadScheduledExecutor
:使用的无界的延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
1 | // 无界队列 LinkedBlockingQueue |
如何设置线程池的核心线程数量
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N
这是一个不会错得很离谱的经验值,由于应用中可能存在多个线程池,以及具体场景的不同,因此合理的线程数需要压测才能决定。
线程池的状态和生命周期
线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。
1 | //线程池状态 |
RUNNING
- 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
- 调用线程池的 shutdown()方法,可以切换到 SHUTDOWN 状态;
- 调用线程池的 shutdownNow()方法,可以切换到 STOP 状态;
SHUTDOWN
- 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
- 队列为空,并且线程池中执行的任务也为空,进入 TIDYING 状态;
STOP
- 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
- 线程池中执行的任务为空,进入 TIDYING 状态;
TIDYING
- 该状态表明所有的任务已经运行终止,记录的任务数量为 0。
- terminated()执行完毕,进入 TERMINATED 状态
TERMINATED
- 该状态表示线程池彻底终止