Skip to content

在Java中,实现并发程序的主要手段就是利用多线程。那如何创建多线程呢?你肯定说这我知道:继承Thread类或者实现Runnable接口,高级一点的使用Executors构建一个线程池然后executorService.execute()。但要是被问到线程池是如何工作的?ThreadPoolExecutor有哪些参数?都是干嘛的?你可能会一问三不知,所以下面我就来和你一起聊一聊这些问题。

大纲

一、线程

1.1 理解线程

很多人线程进程傻傻分不清楚,其实他们压根不是一类东西。程序启动创建了进程,进程里面可以有很多线程,进程是线程的容器。线程是操作系统能够进行运算调度的最小单位,而进程则是一个运行中的程序实例。白话来说就是进程申请创建了容器,而线程则是实际使用容器的。

1.2 线程的生命周期

Java线程的状态有 NEW(初始化状态)RUNNABLE(可运行 / 运行状态)BLOCKED(阻塞状态)WAITING(无时限等待)TIMED_WAITING(有时限等待)TERMINATED(终止状态)这六种,看着挺复杂,来一张图你就能看懂。

线程状态流转图

二、线程池

2.1 池化思想

线程池(Thread Pool)是一种基于池化思想管理线程的工具,池化思想是一种典型的使用空间换时间来提升性能的方法,常见的使用池化思想来管理资源的除了线程池外还有连接池对象池等。

池化思想优点:

  • 资源可复用,降低资源创建和销毁锁带来的损耗
  • 提升响应速度,预先创建,任务到来直接使用资源无需临时创建
  • 资源的可管理可扩展可监控

池化思想缺点: 池化思想的缺点也很明显,因为是利用空间换时间,所以如果创建不使用则会造成浪费,预先创建的则会拖慢启动。

2.2 线程池参数详解

在聊线程池参数前,我们先来看一下Java 线程池实现类 ThreadPoolExecutor 的类图

线程池类图

从上图我们可以看到,ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。接口ExecutorService提供了管理终止的方法,以及提供对Executor.execute方法的扩展方法submit使其可以生成Future。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

接下来我们来一起看一下ThreadPoolExecutor的构造方法

线程池够造方法图

来逐一解释一下每个参数的含义:

corePoolSize

表示线程池保有的最小线程数。就像有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地。

maximumPoolSize

表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人。

keepAliveTime 和 unit

上面提到项目根据忙闲来增减人员,那如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit就是用来定义这个"一段时间"的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了

workQueue

BlockingQueue 工作队列

threadFactory

通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字

handler

通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略:

  • CallerRunsPolicy:提交任务的线程自己去执行该任务。
  • AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
  • DiscardPolicy:直接丢弃任务,没有任何异常抛出。
  • DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

2.3 任务执行机制详解

提交任务的过程

线程池任务提交过程

线程池生命周期

状态状态描述
RUNNING接受新任务并处理排队的任务
SHUTDOWN不接受新任务,处理排队的任务
STOP不接受新任务,不处理排队的任务,中断正在进行的任务
TIDYING所有任务都已终止,workerCount为零,状态转换到 TIDYING 将运行 terminate() 钩子方法
TERMINATEDterminated 方法已经执行完成

流转过程如下

线程池生命周期

增加工作线程

增加线程流程图

工作线程任务执行

任务执行流程图

三、线程池工厂Executors

Executors是JDK提供的线程池工厂类,可以很方便的创建一个具有特定功能的线程池。下面我们聊一聊它具体可以提供了哪些功能的线程池,有什么优缺点。

方法功能缺点
newFixedThreadPool固定线程数量的线程池,队列为LinkedBlockingQueue长度为Integer.MAX_VALUE队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
newSingleThreadExecutor单线程的线程池,队列为LinkedBlockingQueue长度为Integer.MAX_VALUE队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
newCachedThreadPoolcorePoolSize为零,maximumPoolSize为 Integer.MAX_VALUE, 队列为SynchronousQueue,线程可弹性扩展,无可复用线程则新增线程允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM或过度切换。
newScheduledThreadPool固定核心线程数量的调度线程池,提供延迟调度和周期调度

四、线程池使用的一些建议

线程池大小设置多少合理?

这是个老生常谈的问题, 我先给个业界比较认可的公式:

CPU 密集型的计算场景,理论上"线程的数量 =CPU 核数"就是最合适的。不过在工程上,线程的数量一般会设置为"CPU 核数 +1"。

I/O 密集型,最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]。

然后说一下我的看法:我觉得没必要纠结大小,可以根据经验先设置一个,然后通过线程池监控 监控线程池的运行情况,然后动态的调整线程池的运行参数。

线程池应该设置为静态私有成员变量吗?

应该,切勿在方法内部声明局部变量的线程池,线程池核心线程在不允许回收的时候,线程池不会被回收。

线程池的异常处理

executorService.submit()方法会吞掉异常,使用submit方法需要调用future.get();

总结

线程池是 Java 并发编程中的核心组件,理解它的工作原理和参数配置对于编写高性能的并发程序至关重要。通过合理配置线程池参数,我们可以有效利用系统资源,提高应用程序的性能和稳定性。

参考资料

相关文章

Released under MIT License.