一、介绍
Java线程池是一个管理线程的工具,它能够显著提高多线程程序的性能和可管理性。线程池是Java中用于管理和控制一组线程的机制,它可以有效地减少创建和销毁线程的开销,提高系统性能。线程池通过重复使用已存在的线程来执行新的任务,避免了频繁地创建和销毁线程所带来的资源消耗。
- 优势特点:
- 降低资源消耗:避免过度创建线程导致的系统开销。
- 提升响应速度:可以快速分配线程来执行任务,提高程序响应效率。
- 增强可管理性:提供统一管理线程的手段,方便监控和调优。
二、线程池的创建方式
线程池的实现一般通过Executors
去创建,或者通过 ThreadPoolExecutor
构造函数的方式。
《阿里巴巴 Java 开发手册》强制线程池不允许使用 Executors
去创建,而是通过 ThreadPoolExecutor
构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors
返回线程池对象的弊端如下(后文会详细介绍到):
FixedThreadPool
和SingleThreadExecutor
:使用的是无界的LinkedBlockingQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。CachedThreadPool
:使用的是同步队列SynchronousQueue
, 允许创建的线程数量为Integer.MAX_VALUE
,可能会创建大量线程,从而导致 OOM。ScheduledThreadPool
和SingleThreadScheduledExecutor
: 使用的无界的延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
实现代码如下:
package com.dream.spi.thread;
import java.util.concurrent.*;
/**
* @author 思维穿梭
*/
public class ThreadPool {
static class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
// 创建一个固定大小的线程池,里面有2个线程
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(new MyThread("fixed "));
executorService.shutdown();
// 创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程
// 没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟
// 未被使用的线程。
ExecutorService executorService1 = Executors.newCachedThreadPool();
executorService1.execute(new MyThread("cached "));
executorService1.shutdown();
// 创建一个单线程化的Executor。
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
executorService2.execute(new MyThread("single "));
executorService2.shutdown();
// 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ScheduledExecutorService executorService3 = Executors.newScheduledThreadPool(2);
Runnable task = () -> System.out.println("Hello, newScheduledThreadPool!");
executorService3.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS);
// executorService3.shutdown();
// 创建一个单线程化的Executor,所有提交的任务都将被序列化处理。
ScheduledExecutorService executorService4 = Executors.newSingleThreadScheduledExecutor();
// 定义一个Runnable任务
Runnable task1 = () -> System.out.println("Hello, newSingleThreadScheduledExecutor!");
// 使用执行器调度任务,延迟1秒后执行,每隔2秒执行一次
executorService4.scheduleAtFixedRate(task1, 1, 2, TimeUnit.SECONDS);
// executorService4.shutdown();
/**
* AbortPolicy:这是默认的策略。当线程池无法接受新任务时,
* 会抛出RejectedExecutionException异常并中止任务的执行。调用者需要处理这个异常。
* DiscardPolicy:这种策略会静默丢弃任务,不会抛出任何异常,也不会做其他处理。
* DiscardOldestPolicy:此策略会丢弃队列中最旧的任务,以便为新被拒绝的任务腾出空间。
* 被丢弃的任务不会被执行,而且也不会有任何通知。
* CallerRunsPolicy:在这种策略下,如果线程池不能再接收新任务,
* 那么提交任务的调用线程将自行执行该任务。
*/
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,3,0, TimeUnit.SECONDS
, new LinkedBlockingDeque<Runnable>(3), Executors.defaultThreadFactory()
,new ThreadPoolExecutor.CallerRunsPolicy());
poolExecutor.submit(()->{
System.out.println("ThreadPoolExecutor " + Thread.currentThread().getName());
});
poolExecutor.shutdown();
}
}
三、Executors框架介绍
Executor
框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor
框架让并发编程变得更加简单。
Executor
框架结构主要由三大部分组成:
1、任务(Runnable
/Callable
)
执行任务需要实现的 Runnable
接口 或 Callable
接口。Runnable
接口或 Callable
接口 实现类都可以被 ThreadPoolExecutor
或 ScheduledThreadPoolExecutor
执行。
2、任务的执行(Executor
)
如下图所示,包括任务执行机制的核心接口 Executor
,以及继承自 Executor
接口的 ExecutorService
接口。ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
这两个关键类实现了 ExecutorService
接口。
3、异步计算的结果(Future
)
Future
接口以及 Future
接口的实现类 FutureTask
类都可以代表异步计算的结果。
当我们把 Runnable
接口 或 Callable
接口 的实现类提交给 ThreadPoolExecutor
或 ScheduledThreadPoolExecutor
执行。(调用 submit()
方法时会返回一个 FutureTask
对象)
4、Executors中内置的线程池
newFixedThreadPool(int nThreads)
:创建一个固定大小的线程池,可以同时执行指定数量的任务。newCachedThreadPool()
:创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程数,那么就会回收部分空闲(60秒不执行任务)的线程。newSingleThreadExecutor()
:创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。newScheduledThreadPool(int corePoolSize)
:创建一个定长线程池,支持定时及周期性任务执行。延迟启动,定期检查是否有需要执行的任务。
源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
其中,ScheduledThreadPool
是通过 ScheduledThreadPoolExecutor
创建的,使用的DelayedWorkQueue
(延迟阻塞队列)作为线程池的任务队列。DelayedWorkQueue
的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue
添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE
,所以最多只能创建核心线程数的线程。
其他FixedThreadPool和CachedThreadPool都是通过ThreadPoolExecutor的构造方法创建的。
SingleThreadExecutor中通过FinalizableDelegatedExecutorService创建,参数是核心线程数和最大线程数都是1的ThreadPoolExecutor对象。
四、ThreadPoolExecutor介绍
线程池实现类 ThreadPoolExecutor
是 Executor
框架最核心的类。
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor
3 个最重要的参数:
corePoolSize
: 任务队列未达到队列容量时,最大可以同时运行的线程数量。maximumPoolSize
: 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue
: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数 :
keepAliveTime
:线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁。unit
:keepAliveTime
参数的时间单位。threadFactory
:executor 创建新线程的时候会用到。handler
:饱和策略。关于饱和策略下面单独介绍一下。
ThreadPoolExecutor
饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,可以选择这个策略。ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
:此策略将丢弃最早的未处理的任务请求。
五、使用线程的注意事项
1、正确的声明线程池:
线程池必须手动通过 ThreadPoolExecutor
的构造函数来声明,避免使用Executors
类创建线程池,会有 OOM 风险。
除了避免 OOM 的原因之外,不推荐使用 Executors
提供的两种快捷的线程池的原因还有:
- 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
- 我们应该显示地给我们的线程池命名,这样有助于我们定位问题
2、监控线程池状态
通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。
除此之外,我们还可以利用 ThreadPoolExecutor
的相关 API 做一个简陋的监控。从下图可以看出, ThreadPoolExecutor
提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。
3、不同的业务使用不同的线程池
一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。
4、正确配置线程池参数
有一个简单并且适用面比较广的公式:
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
线程数更严谨的计算的方法应该是:最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))
,其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)
。
线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。
我们可以通过 JDK 自带的工具 VisualVM 来查看 WT/ST
比例。
CPU 密集型任务的 WT/ST
接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。
IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。
一些开源项目可以实现动态配置核心参数:
- Hippo4jopen in new window:异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。
- Dynamic TPopen in new window:轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。
5、别忘记关闭线程池
当线程池不再需要使用时,应该显式地关闭线程池,释放线程资源。
线程池提供了两个关闭方法:
shutdown()
:关闭线程池,线程池的状态变为SHUTDOWN
。线程池不再接受新任务了,但是队列里的任务得执行完毕。shutdownNow()
:关闭线程池,线程池的状态变为STOP
。线程池会终止当前正在运行的任务,停止处理排队的任务并返回正在等待执行的 List。
调用完 shutdownNow
和 shuwdown
方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination
方法进行同步等待。在调用 awaitTermination()
方法时,应该设置合理的超时时间,以避免程序长时间阻塞而导致性能问题。
6、Spring中手动创建线程池
使用 Spring 内部线程池时,一定要手动自定义线程池,配置合理的参数,不然会出现生产问题(一个请求创建一个线程)。