线程池详解
1 什么是线程池?
线程池的起源是因为对象创建的开销过大,于是便有了池化的思想,一个线程用完后不去销毁而是放到池中等待为下一个对象服务。其优点显而易见:
- 降低资源消耗:通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时可以不需要等待线程创建完之后再执行。
- 提高线程的可管理性:使用线程池可以进一步对线程进行分配、调优和监控。
2 Executor框架
Java5引入的框架,可以创建和启动线程。建议使用Executor来启动线程,比Thread类的start方法能好一点,除了易于管理外还能避免this逃逸问题(是指在类的构造方法尚未执行完成的时候,另一个类采用该类.this就能获取该类的对象,而此时外围类对象可能还没构造完成,就出现了所谓的this逃逸问题。)
2.1 Executor框架结构
-
任务(Runnable/Callable)
执行任务需要实现的接口
-
任务的执行(Executor)
-
异步计算的结果(Future)
Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。
2.2 Executor框架的使用示意图
-
主线程首先创建实现Runnable或者Callable接口的任务对象。二者区别如下:
Runnable自Java1.0以来就一直存在,但Callable仅在1.5中引入,目的是为了来处理Runnable不支持的用例。Runnable接口不会返回结果或抛出检查异常,但是Callable接口可以。所以任务不需要返回结果或抛出异常的话建议使用Runnable,这样代码看起来更简洁。
-
把创建完成的对象直接交给ExecutorService来执行:execute(Runnable command)或者submit(Runnable task)。
-
如果执行了submit方法,说明该任务需要返回值,ExecutorService将返回一个实现Future接口的对象,通过这个对象可以判断任务是否执行成功,可以通过Future的get方法来获取返回值;而execute方法用于不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
-
最后主线程可以执行FetureTask.get()方法来等待任务执行完成。也可以执行FutureTask.cancel()来取消此任务的执行。
3 ThreadPoolExecutor类简单介绍
线程池的实现类,是Executor框架最核心的类。其构造方法有7个参数:
- corePoolSize:线程池的核心线程数,定义了可以同时运行的线程数量。
- maxmumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变成最大线程数。
- workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到新任务就会被放到队列中。
- keepAliveTime:前面可以看出线程池中的线程数量可能会变成最大线程数,此时当线程数量大于核心线程数且没有新任务提交,核心线程数外的线程不会立即销毁,而是会等待一个存活时间才会被回收销毁。
- unit:keepAliveTime参数的时间单位。
- threadFactory:executor创建新线程的时候会用到。
- handler:饱和策略。(较为重要,单独说)
3.1 饱和策略
考虑一种极端情况:如果任务队列满了,且线程池当前同时运行的线程数达到最大线程数量的时候,ThreadPoolTaskExecutor定义了一些策略:
- ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程上运行run来执行被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务的提交速度,影响程序的整体性能。如果程序中可以承受此延迟并且你要求任何一个任务请求都要被执行的话就选择这个策略。
- ThreadPoolExecutor.DiscardPolicy:不出来新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy:此策略丢弃最早的未处理的任务请求。
3.2 推荐使用ThreadPoolExecutor构造函数创建线程池
在《阿里巴巴Java开发手册》中明确禁止使用Executors类来创建线程池,强制使用ThreaPoolExecutor来创建,目的是为了开发人员明确每个参数的意义,灵活的设置管理线程池。
4 ThreadPoolExecutor使用示例
4.1 示例代码:Runnable+ThreadPoolExecutor
首先创建一个Runnable接口的实现类。(也可以是Callable接口)
MyRunnable.java
import java.util.Date;
/**
* Created by Yinlu on 2021/6/30
* 这是一个简单的Runnable类,需要大约5s时间来执行其任务
*/
public class MyRunnable implements Runnable{
private String command;
public MyRunnable(String command) {
this.command = command;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "MyRunnable{" +
"command='" + command + '\'' +
'}';
}
}
编写测试程序ThreadPoolExecutorDemo.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Created by Yinlu on 2021/6/30
*/
public class ThreadPoolExecutorDemo {
// 设置各个参数
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 10; i++) {
// 创建WorkerThread对象(该类实现了Runnable接口)
Runnable worker = new MyRunnable("" + i);
// 执行Runnable
executor.execute(worker);
}
// 终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
执行步骤如下:
4.2 几个常见的对象
-
Runnable和Callable
Runnable从1.0开始就有,只有execute方法,没有返回值,Callable从5.0开始引入,用来处理Ruunable无法处理的任务,有返回值。
-
execute()和submit()
第一个没有返回值,是Runnable接口的方法;第二个有返回值,是callable的方法。
-
shutdown()和shutdownNow()
- shutdown:关闭线程池,线程池的状态变成SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
- shutdownNow:关闭线程池,线程池的状态变成stop。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的List。
-
isTerminated()和isShutdown()
- isShutdown:当调用shutdown方法返回true
- isTerminated:当调用shutdown方法后,并且所有提交的任务完成后返回true
5 几种常见的线程池详解
5.1 FixedThreadPool
FixedThreadPool被称为可重用固定线程数的线程池。通过Executors类中的相关源代码可以看到:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
其中参数corePoolSize和maxmumPoolSize都被设置成nThreads。
FixedThreadPool的execute()方法运行示意图:
说明:
- 如果当前运行的线程数小于corePoolSize,如果再来新任务的话,就创建新的线程来执行任务;
- 当前运行的线程数等于corePoolSize后,如果再来新任务,会将任务添加到LinkedBlockingQueue;
- 线程池中的线程执行完手头的任务就会依次从队列中获取任务来执行。
5.1.1 FixedThreadPool的缺点
FixedThreadPool使用无界队列LinkendBlockingQueue(队列的容量为Integer.MAX_VALUE)作为线程池的工作队列会对线程池造成如下影响:
- 当线程池中的线程池达到corePoolSize后,新任务将一直往无界队列中放,因此线程池中的线程数不会超过corePoolSize;
- 由于使用无界队列,maximumPoolSize将是一个无效的参数,因为不可能存在任务队列满的情况。
- 由于1和2,使用无界队列,keepAliveTime将是一个无效的参数;
- 运行中的FixedThreadPool(未执行shutdown或者shutdownNow)不会拒绝任务,在任务比较多的时候会导致OOM。
5.2 SingleThreadPoolExecutor详解
SingleThreadPoolExecutor是只有一个线程的线程池:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
其中corePoolSize和maximumPoolSize都被设置成1,其他参数和FixedThreadPool相同。
运行示意图如下:
说明:
- 当线程池中运行的线程数小于corePoolSize,则创建一个新的线程执行任务;
- 当线程池中有一个已经运行的线程时,将任务添加到LinkedBlockingQueue中;
- 当前线程执行完任务后,依次从队列中获取新的任务执行。
5.2.1 SingleThreadPoolExecutor的缺点
同样会导致OOM。
5.3 CachedThreadPool详解
CachedThreadPool是一个会根据需要创建新线程的线程池:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
其中,corePoolSize被设置为0,maximumPoolSize被设置成Integer.MAX_VALUE,意味着如果主线程提交任务的速度高于maximumPool中的线程处理任务的速度时,CachedThreadPool会不断的创建新的线程。极端情况下会导致耗尽cpu和内存资源。
CachedThreadPool的execute方法执行示意图:
说明:
- 首先执行SynchronousQueue.offer(Runnable task)提交任务到任务队列。如果当前maximumPool有闲线程正在执行SynchronousQueue.poll(),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute方法执行完成,否则执行步骤2;
- 当厨师maximumPool为空,或者maximumPool中没有空闲线程时,将没有线程执行poll方法。这种情况步骤1将失败,此时CachedThreadPool会创建新线程执行任务,execute方法执行完成。
5.3.1 缺点
CachedThreadPool允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。
5.4 ScheduledThreadPoolExecutor详解
主要用于在给定的延迟后运行任务,或者定期执行任务。在实际项目中基本不会被用到。
5.4.1 简介
ScheduledThreadPoolExecutor使用的任务队列DelayQueue封装了一个PriorityQueue,PriorityQueue会对队列中的任务进行排序,执行所需时间短的放在前面执行,如果执行所需时间相同则先提交的任务将被先执行。
注:Quartz是一个由Java编写的任务调度库,在实际开发使用居多。
运行机制:
5.5 线程池中三种队列的区别
-
SynchronousQueue
SynchronousQueue没有容量,是一个无缓冲阻塞队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中被添加的元素被消费后才能继续添加新的元素。用于公平策略和非公平策略,使用该队列一般会将maximumPoolSize设置为Integer.MAX_VALUE,避免线程拒绝执行操作。例如CachedThreadPool。
-
LinkedBlockingQueue
LinkedBlockingQueue是一个无界缓存阻塞队列,使用该队列maximumPoolSize就相当于无效了(注:默认情况下容量是Integer.MAX_VALUE,但是开发人员也可以自己设置大小)。
-
ArrayBlockingQueue
ArrayBlockingQueue是一个有界缓存等待队列,当线程池中正在运行的线程数大于corePoolSize时,新任务会往该队列中添加等待有空闲的线程时会执行,如果该队列满了后线程池中会创建新的线程来执行此任务,如果线程数达到了maximumPoolSize,会执行拒绝策略。
6 线程池大小的确定
线程池的理想大小取决于被提交任务的类型以及所部署系统的特性,在代码中通常不会固定线程池的大小,而是应该通过某种机制来提供,或者根据Runtime.availableProcessors来动态计算。
要想正确设置线程池大小,必须分析计算环境、资源预算和任务的特性。在部署的系统中有多少个cpu?多大的内存?任务是计算密集型、I/O密集型还是二者皆可?
计算密集型:计算密集型就是计算、逻辑判断量非常大而且集中的类型,因为主要占用cpu资源所以又叫cpu密集型,而且当计算任务数等于cpu核心数的时候,是cpu运行效率最高的时候。
I/O密集型:IO密集型就是磁盘的读取数据和输出数据非常大
对于计算密集型的任务,在拥有Ncpu个处理器的系统上,当线程池大小为Ncpu+1时,通常能实现最优的利用率。
对于包含I/O操作或者其他阻塞操作的任务,由于线程不会一直执行,因此线程池的规模应该更大。要正确的设置线程池的大小,必须估算任务的等待时间与计算时间的比值。(也有设置为2N的)给出以下定义:
Ncpu=number of CUPs
Ucpu=target CPU utilization, 0≤Ucpu≤1
W/C=ratio of wait time to compute time
要使处理器达到期望的使用率,线程池的最优大小等于
Nthreads = Ncpu*Ucpu * (1+W/C)可以通过下面代码来获得CPU的数目
int N_CPUS = Runtime.getRuntime().availableProcessors();
不会一直执行,因此线程池的规模应该更大。要正确的设置线程池的大小,必须估算任务的等待时间与计算时间的比值。(也有设置为2N的)给出以下定义:
Ncpu=number of CUPs
Ucpu=target CPU utilization, 0≤Ucpu≤1
W/C=ratio of wait time to compute time
要使处理器达到期望的使用率,线程池的最优大小等于
Nthreads = Ncpu*Ucpu * (1+W/C)可以通过下面代码来获得CPU的数目
int N_CPUS = Runtime.getRuntime().availableProcessors();