线程池
一、线程池的优势
池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
1.1 使用线程池的好处:
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
- 提供更强大的功能,延时定时线程池。
二、Executor框架
2.1 Exectuor框架结构
主要由三部分组成
-
任务(Runnable/Callable)
被执行的任务需要实现Runnable或者是Callable接口,任务可以被ThreadPoolExecutor或 ScheduledThreadPoolExecutor执行 -
任务的执行(Exectuor)
通过查看 ScheduledThreadPoolExecutor 源代码我们发现 ScheduledThreadPoolExecutor 实际上是继承了 ThreadPoolExecutor 并实现了 ScheduledExecutorService ,而 ScheduledExecutorService 又实现了 ExecutorService,正如我们下面给出的类关系图显示的一样。 -
异步执行计算结果(Future)
Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。
当我们把 Runnable接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象)
2.2 Exectuor框架的使用
- 主线程首先创建实现了Runnable或者Callable结果的任务对象。
- 把创建好的任务对象,交给ExectuorService执行ExecutorService.execute(Runnable command)或者执行ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable task)
ExecutorService.execute方法和ExecutorService.submit方法的对比
- 二者可以接受的参数不同,submit可接受Runnable和Callable接口对象,而execute只可以接受Runnable接口对象
- submit方法执行有返回值,execute没有
-
如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
-
最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
三、ThreadPoolExecutor类
3.1 构造方法的参数介绍
/**
* 用给定的初始参数创建一个新的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 饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:
- ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
四、ThreadPoolExectuor的使用
另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
示例
首先创建一个实现了Runnable接口的类用来执行。(当然也可以是 Callable 接口,我们上面也说了两者的区别。)
/**
* @date 2021/2/7 21:38
*/
public class MyRunnable implements Runnable{
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
}
编写线程池程序来执行任务
/**
* @date 2021/2/7 21:46
*/
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5; // 核心线程数为 5。
private static final int MAX_POOL_SIZE = 10; // 最大线程数 10
private static final int QUEUE_CAPACITY = 100; // 任务队列容量为100
private static final Long KEEP_ALIVE_TIME = 1L; // 等待时间为 1L。
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_POOL_SIZE ,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS, // 等待时间的单位为 TimeUnit.SECONDS。
new ArrayBlockingQueue<>(QUEUE_CAPACITY), // 任务队列为 ArrayBlockingQueue
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable(""+i);
executor.execute(worker);
}
executor.shutdown(); // 下面会讲解
while (!executor.isTerminated()){} // 下面会讲解
System.out.println("Finished all threads");
}
}
可以看到我们上面的代码指定了:
- corePoolSize: 核心线程数为 5。
- maximumPoolSize :最大线程数 10
- keepAliveTime : 等待时间为 1L。
- unit: 等待时间的单位为 TimeUnit.SECONDS。
- workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
- handler:饱和策略为 CallerRunsPolicy。
执行结果如下
pool-1-thread-5 Start. Time = Mon Feb 08 11:09:05 CST 2021
pool-1-thread-4 Start. Time = Mon Feb 08 11:09:05 CST 2021
pool-1-thread-2 Start. Time = Mon Feb 08 11:09:05 CST 2021
pool-1-thread-3 Start. Time = Mon Feb 08 11:09:05 CST 2021
pool-1-thread-1 Start. Time = Mon Feb 08 11:09:05 CST 2021
pool-1-thread-4 End. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-2 End. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-1 End. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-3 End. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-5 End. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-3 Start. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-1 Start. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-2 Start. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-4 Start. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-5 Start. Time = Mon Feb 08 11:09:10 CST 2021
pool-1-thread-5 End. Time = Mon Feb 08 11:09:15 CST 2021
pool-1-thread-4 End. Time = Mon Feb 08 11:09:15 CST 2021
pool-1-thread-1 End. Time = Mon Feb 08 11:09:15 CST 2021
pool-1-thread-2 End. Time = Mon Feb 08 11:09:15 CST 2021
pool-1-thread-3 End. Time = Mon Feb 08 11:09:15 CST 2021
Finished all threads
Process finished with exit code 0
我们可以看到,线程首先执行5个任务,因为我们设置的核心线程数为 5,然后这些任务执行完,我们会继续在队列中拿新的任务去执行
小贴士:
- shutdown()和shutdownNow()的区别
- shutdown() : 当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
- shutdownNow() : 执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
- isShutDown、isTerminated的区别
- isShutDown:当调用shutdown()或shutdownNow()方法后返回为true。
- isTerminated:当调用shutdown()方法后,并且所有提交的任务完成后返回为true。
五、几种常见线程池详解
线程池名称 | 描述 |
---|---|
FixedThreadPool | 核心线程数与最大线程数相同 |
SingleThreadExecutor | 一个线程的线程池 |
SingleThrCachedThreadPooleadExecutor | 核心线程为0,最大线程数为Integer. MAX_VALUE |
ScheduledThreadPool | 定核心线程数的定时线程池 |
SingleThreadScheduledExecutor | 单例的定时线程池 |
ForkJoinPool | JDK 7 新加入的一种线程池 |
5.1 FixedThreadPool
5.1.1 FixedThreadPool 被称为可重用固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
从上面源代码可以看出,FixThreadPool的corePoolSize 和 maximumPoolSize都设置为nThreads,这个 nThreads 参数是我们使用的时候自己传递的
5.1.2 使用说明:
- 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
- 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue;
- 线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;
5.1.3 FixedThreadPool的缺点
FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
- 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
- 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
- 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
- 运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。
5.2 SingleThreadExecutor
5.2.1 SingleThreadExecutor: 又称一个线程的线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
从源代码可以看出,SingleThreadExecutor将corePoolSize 和 maximumPoolSize都设置为1
5.2.2 使用说明
- 如果当前运行的线程数少于1个,则创建一个新的线程执行任务;
- 当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue
- 线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue 中获取任务来执行;
5.2.3 SingleThreadExecutor的缺点
SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM。
5.3 CachedThreadPool
5.3.1 CachedThreadPool 是一个会根据需要创建新线程的线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
5.2.2 使用说明
- 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 CachedThreadPool 中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2;
- 当初始 CachedThreadPool 为空,或者 CachedThreadPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;
5.3.3 CachedThreadPool的缺点
从上面可以看出,初始化corePoolSize为0,maximumPoolSize被设置为 Integer.MAX.VALUE,即它是无界的,需要时才创建新线程,这也就意味着如果主线程提交任务的速度高于 CachedThreadPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
5.4 ScheduledThreadPoolExecutor
六、线程池的大小确认
大部分程序员在设定线程池大小的时候就是随心而定。很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。对于多线程来说线程越多主要是增加了上下文切换成本。
6.1 有一个简单并且适用面比较广的公式:
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
6.2 如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。