文章目录
学习线程池之前,首先我们需要明白一个问题——为什么需要线程池?在Java中使用线程来执行异步任务时,线程的创建和销毁需要一定的开销,如果我们为每一个任务都创建一个新的线程来执行的话,这些线程的创建和销毁都需要消耗大量系统资源,同时将会使服务器处于高负荷状态。所以才会出现了线程池,线程池可以理解为使用了原型模式,我们可以提前在线程池中创建若干条线程,当有任务需要执行时,就从线程池中取出一条线程处理任务,线程执行完任务之后也并不会直接销毁,而是重新放在线程池中,等待处理将来可能达到的任务。
如果同一时刻并发量很大,线程池中所有线程都处于忙碌状态,那么后面的任务就进入一个等待队列等待,知道线程池中的线程处理完手头的任务之后,就可以从等待队列中获取需要执行任务并处理。如此循环往复,就不再需要频繁的进行线程的创建和销毁,缓解我们服务器在高并发场景下的压力
一、Executor
在笔者的多线程—Java内存模型与线程文章中,我们探讨了Java中线程的实现是基于操作系统的原生线程模型来实现,即采用 1:1 的线程模型,操作系统会调度所有的线程并将它们分给可用的 CPU,当Java线程终止时,操作系统线程也会被回收。
使用Executor
框架进行任务处理的映射方式是这样实现的:在上层,Java多线程服务器程序把大量用户 client 发送的请求分为若干个任务,然后使用Executor
调度器将这些任务映射为固定数量的线程;在底层,操作系统会将这些线程映射到哦硬件处理器上,这种任务调度模型如下图所示:
从图中我们可以看出,应用程序通过Executor框架控制上层的调度,而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。
Executor 是一个接口,其源码如下:
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
Executor
顾名思义,它就是执行器的意思,可以发现其内部只有一个方法 execute()
,其功能就是执行任务。以前我们使用线程处理任务,需要new一个线程、重写run()方法、并调用start()方法执行。现在有了 Executor 之后,任务的定义(实现 Runnable 的任务)和任务的执行就可以分离开来,进行异步处理,提高系统响应能力。
二、ExecutorService
ExecutorService
是从Executor
接口继承,它除了实现execute()
方法可以执行一个任务之外呢,它还完善了整个任务执行器的生命周期,相当于它拓展了Executor
接口。实际上线程池的实现就是基于 ExecutorService
接口的基础之上实现的
ExecutorService
源码如下:
public interface ExecutorService extends Executor {
//结束
void shutdown();
//马上结束
List<Runnable> shutdownNow();
//是否结束
boolean isShutdown();
//整体是否都执行完了
boolean isTerminated();
//等待结束,指定时间
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
//submit提交一个实现Callable接口的任务,并且返回封装了异步计算结果的Future。
<T> Future<T> submit(Callable<T> task);
//submit提交一个实现Runnable接口的任务,并且指定了在调用Future的get方法时返回的result对象。
<T> Future<T> submit(Runnable task, T result);
//submit提交一个实现Runnable接口的任务,并且返回封装了异步计算结果的Future。
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
}
观察源码我们可以发现,可以发现ExecutorService
接口继承自Executor
接口,它必然有execute()
方法,用来执行任务,除此之外,它还定义了如下提交任务的方法:
- Future submit(Callable task):
- Future submit(Runnable task, T result):
- Future<?> submit(Runnable task);
主线程使用线程池处理任务,通过调用submit
提交任务之后,主线程就该干嘛就干嘛,不需要阻塞在这里等待任务执行结束和获得结果,所以说submit()
方法实现了异步处理任务。
上述方法参数中,Runnable
我们是认识的,可是Callable
和Future
是个什么东西呢?下面我们就来探讨一下
三、Callable、Future和FutureTask
在笔者的多线程—Java内存模型与线程文章中,我们知道创建线程有两种方式,一种是实现Runnable
接口,另一种是实现Thread
类。但是这两种方式都有个缺点,那就是在任务执行完成之后无法获取返回结果,如果我们想要获取任务返回值结果,应该怎么办呢?
从Java 1.5 之后开始引入了Callable
和Future
,通过它们构建的线程,在任务执行完毕之后就可以获取结果,下面我们就来聊聊创建线程的第三种方式,那就是实现Callable
接口
1、Callable接口
我们先回顾一下java.lang.Runnable
接口,此接口只声明了run()
方法,返回值为void
,当然就无法获取结果了
public interface Runnable {
public abstract void run();
}
而Callable
的接口定义如下
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
此接口声明了一个名称为call()的方法,这个方法有返回值V,同时此方法也能抛出异常。在上面我们知道无论是Runnable
接口的实现类还是Callable
接口的实现类,都可以被ExecutorService
执行,我们再来回顾一下ExecutorService
接口提供的方法:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
- 第一个方法:submit提交一个实现
Callable
接口的任务,并且返回封装了异步计算结果的Future。 - 第二个方法:submit提交一个实现
Runnable
接口的任务,并且指定了在调用Future的get方法时返回的result对象。 - 第三个方法:submit提交一个实现
Runnable
接口的任务,并且返回封装了异步计算结果的Future。
因此我们只要创建好我们的线程对象(实现Callable接口或者Runnable接口),然后通过上面3个方法提交给实现了ExecutorService
接口的线程池去执行即可。
2、Future接口
Future接口是用来封装异步计算结果的,说白了就是对具体的Runnable
或者Callable
对象任务执行的结果进行get()获取、cancel()取消、isDone() 判断是否完成等操作。Future
接口的源码也不是很难,下面我们来看一下Future
接口的源码:
public interface Future<V> {
//如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;
boolean cancel(boolean mayInterruptIfRunning);
//如果任务完成前被取消,则返回true。
boolean isCancelled();
//如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
boolean isDone();
//获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
V get() throws InterruptedException, ExecutionException;
//获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
通过分析我们可以发现实际上Future提供了三种功能:
1. 能够中断执行中的任务;
2. 能够判断任务是否执行完成;
3. 能够获取任务执行完成后的结果。
但是我们需要明白Future只是一个接口,我们无法直接创建一个对象,因此就需要其实现类FutureTask
创建对象
3、FutureTask类
FutureTask的源码比较复杂,我们先来简单看一下FutureTask
类的源码实现:
public class FutureTask<V> implements RunnableFuture<V> {
public FutureTask(Callable<V> callable) {
...
}
public FutureTask(Runnable runnable, V result) {
...
}
...
public void run() {
...
}
......
}
可以发现FutureTask
类实现了RunnableFuture
接口,我们看一下RunnableFuture
接口的实现:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
可以发现FutureTask
的类图关系如下:
源码分析:FutureTask
除了实现了Future
接口外还实现了Runnable
接口,因此FutureTask
也可以直接提交给Executor
执行。 当然也可以调用线程直接执行(FutureTask.run()
)。接下来我们根据FutureTask.run()
的执行时机来分析任务所处的3种状态:
- 未启动:当创建一个
FutureTask
,但是FutureTask.run()
方法还未被执行前,这个FutureTask处于未启动状态; - 已启动:
FutureTask.run()
被执行的过程中,FutureTask处于已启动状态; - 已完成:
FutureTask.run()
方法执行完正常结束,或者被取消或者抛出异常而结束,FutureTask都处于完成状态。
由于Callable、Future和FutureTask设计出来都是为了构建线程池的,单独很难使用,也很少单独使用,所以这里就不再写演示程序了
四、Executor框架整体结构
学习完Callable
、Future
和Future Task
之后,我们再来探讨一下Executor
框架整体结构
Executor
框架的结构主要包括3个部分:
- 任务Task:被执行的任务需要实现
Runnable
接口或Callable
接口,才可以被Executor
框架当作任务执行 - 任务的执行:包括任务执行机制的核心接口
Executor
,以及继承自Executor
的EexcutorService
接口。Executor
有两个关键类实现了ExecutorService
接口(ThreadPoolExecutor
和ScheduledThreadPoolExecutor
)。 - 异步计算的结果:包括接口
Future
和实现Future
接口的FutureTask
类
这些类之间的继承关系如下图:
类图分析总结:
Executor
是一个接口,它是Executor框架的基础,它将任务的提交很任务的执行分离开来;ThreadPoolExecutor
是线程池的核心实现类,用来提交被执行的任务;ScheduledThreadPoolExecutor
是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令;Future
接口和实现Future
接口的FutureTask
类,代表异步执行的结果;Runnable
接口和Callable
接口的实现类,都可以被ThreadPoolExecutor
或者ScheduledThreadPoolExecutor
执行。区别就是Runnable
无法返回执行结果,而Callable
可以返回执行结果。
下面以一张图来理解Executor
框架处理任务的执行关系:
线程池处理任务,流程分析说明:
- 主线程首先创建实现
Runnable
或者Callable
接口的任务对象 - 把Runnable对象直接提交给
ExecutorService
执行,方法为ExecutorService.execute(Runnable command)
; - 或者也可以把
Runnable
对象或者Callable
对象提交给ExecutorService
执行,方法为ExecutorService.submit(Runnable task)
或者ExecutorService.submit(Callable<T> task)
; - 由于
FutureTask
实现了Runnable
接口,我们也可以直接创建FutureTask任务,然后提交给ExecutorService
执行
到此为止,Executor
框架的主要体系结构我们都学习完了。可以知道任务最终都是提交给ExecutorService
执行的,而ExecutorService
是一个接口,需要有具体的子类实现来执行任务。
下面我们就来重点探讨一下ExecutorService
的三个主要的线程池实现类ThreadPoolExecutor
、ScheduledThreadPoolExecutor
和ForkJoinPool
,常用的线程池都是基于这两个实现类构建的。
五、ThreadPoolExecutor
传说中的线程池7个参数
ThreadPoolExecutor
是线程池的真正实现,通常使用工厂类Executors
来创建,但它的构造方法提供了一系列参数来配置线程池,下面我们就先介绍 ThreadPoolExecutor
的构造方法中各个参数的含义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
(重要)构造函数7个参数解释:
corePoolSize
:线程池的核心线程数,默认情况下,核心线程数会一直在线程池中存活,即使它们处理闲置状态;maximumPoolSize
:线程池所能容纳的最大线程数量,当活动线程数到达这个数值后,后续的新任务将会被放入阻塞队列;keepAliveTime
:非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收;TimeUnit
:用于指定keepAliveTime参数的时间单位,可以指定为毫秒、秒以及分钟等;workQueue
:线程池中的任务队列,通过线程池的execute
方法提交Runnable
对象会存储在这个队列中。有关线程池种使用到的阻塞队列请参考:多线程/并发编程——阻塞队列(简要),不同的线程池使用不同的阻塞队列,有各自的应用场景;ThreadFactory
:线程工厂,为线程池提供创建新线程的功能。ThreadFactory
是一个接口,它只有一个方法:Thread newThread(Runnable r)
,产生线程的方式可以通过自己定义一个ThreadFactory
,来产生你自己特定的线程。默认提供的是defaultFactory,它创建线程的时候要求指定group和name,这样当出现问题的时候,可以根据日志很快的定位到问题所在;RejectedExecutionHandler
:拒绝策略。当ThreadPoolExecutor
已经饱和时(达到了最大线程池大小而且工作队列已经满),execute
方法将会调用Handler
的rejectExecution
方法来通知调用者,我们可以自定义拒绝策略,而默认情况下是抛出一个RejectExecutionException
异常;
(重要)了解完相关构造函数的参数,我们再来看看 ThreadPoolExecutor
执行任务时的大致处理流程:
- 如果线程池的数量还未达到核心线程的数量,那么会直接启动一个核心线程来执行任务;
- 如果线程池中的线程数量已经达到或者超出核心线程的数量,那么任务会被插入到任务队列中排队等待执行;
- 如果在步骤2中无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务;
- 如果在步骤3中线程数量已经达到线程池规定的最大值,那么就会拒绝执行此任务,
ThreadPoolExecutor
会调用RejectExecutionHandler
的rejectExecution
方法来通知调用者。
阿里开发手册要求禁止使用Java提供的线程池,线程池是需要自定义的,所以面试经常会有如下问题:
1、为什么要自定义线程池?
答:创建线程或者线程池时,请指定有意义的线程名称,方便出错时进行回溯。比如你的服务器中有成千上万的线程,有一个线程出错了,在日志中查看出错线程名称是 group1.thread1,根本无法定位到问题之所在,你说崩溃不崩溃。
2、线程池如何自定义创建?
答:阿里开发手册:线程池不允许通过
Executors
(即Java默认提供的)去创建,而是通过ThreadPoolExecutor
(使用上述7个参数)的方式,这样的处理方式让别的成员更加明确线程池的运行规则,规避资源耗尽的问题。说明:
Executors
返回的线程池对象的弊端: 1):
FixedThreadPool
和SingleThreadPool
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2):
CachedThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
小公司使用线程池可能不会出现上述问题,因为没有那么大的用户群体,但是对于体量比较大的公司,比如阿里、百度、京东等,如果线程池使用不当,完全有可能出现上述问题
ThreadPoolExecutor的使用示例:
public class TestThreadPoolExecutor {
//定义一个任务 task
static class Task implements Runnable {
private int i;
public Task(int i) {
this.i = i;
}
@Override
public void run() {
//打印处理任务的线程名称及任务信息
System.out.println(Thread.currentThread().getName() + " Task " + i);
try {
//阻塞模仿处理任务需要时间,使能正确打印阻塞队列中的任务信息
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "Task{" +
"i=" + i +
'}';
}
}
public static void main(String[] args) {
ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 8; i++) {
//线程池处理任务
tpe.execute(new Task(i));
}
//打印队列中的任务
System.out.println(tpe.getQueue());
tpe.shutdown();
}
}
执行结果:
//步骤2:任务添加至阻塞队列
[Task{i=2}, Task{i=3}, Task{i=4}, Task{i=5}]
//步骤1:核心线程处理任务
pool-1-thread-1 Task 0
pool-1-thread-2 Task 1
//步骤3:非核心线程处理任务
pool-1-thread-3 Task 6
pool-1-thread-4 Task 7
到此为止 ThreadPoolExecutor
的构造器细节探讨完了, ThreadPoolExecutor
的执行规则也探讨完了,也简单模拟了如何使用ThreadPoolExecutor
来处理任务。
那么接下来我们就来学习3种常见的线程池,它们都直接或者间接地通过配置 ThreadPoolExecutor
来实现自己独特的功能特性,各有各的使用场景,这个3种线程池分别是:
- FixedThreadPool
- CachedThreadPool
- SingleThreadExecutor
1、FixedThreadPool
FixedThreadPool
用于创建固定线程数量的线程池
创建 FixedThreadPool
对象代码如下:
//通过Executors创建FixedThreadPool线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
我们来看看 FixedThreadPool
创建方法源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
源码分析:可以发现构建此线程池需要传递一个int类型参数,此参数既代表核心线程数有代表最大线程数,所以FixedThreadPool
是用来创建一个固定线程数量的线程池
特点:线程数固定,并不会无限制的消耗系统资源,但是其使用LinkedBlockingQueue
无界队列来存放任务,有可能导致任务积累过多。但是可以根据业务设置合适数量的线程数,性能比较均衡,所以如果要是使用Java提供的线程池的话,FixedThreadPool
是比较普遍使用的线程池
有关线程池中线程数量的设置,有如下建议:
下面我们来看看FixedThreadPool
的execute()
方法的运行流程:
流程分析:
- 如果当前运行线程数少
corePoolSize
,则创建一个新的线程来执行任务; - 如果当前线程池的运行线程数等于
corePoolSize
,那么后面提交的任务将加入LinkedBlockingQueue
; - 线程在执行完图中的1后,会在循环中反复从
LinkedBlockingQueue
获取任务来执行。
2、CachedThreadPool
CachedThreadPool 会不断的创建足够多的线程处理任务(Task)
创建CachedThreadPool代码如下:
//通过Executors创建CachedThreadPool线程池
ExecutorService service = Executors.newCachedThreadPool();
CachedThreadPool创建方法代码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
源码分析:可以发现其corePoolSize
参数指定为0、maximumPoolSize
参数指定为Integer.MAX_VALUE,而任务队列使用SynchronousQueue
,其容量为0。所以只要有任务到达,CachedThreadPool
就会不断地创建新的线程处理任务,直到线程数达到Integer.MAX_VALUE
特点:无核心线程,任务不会阻塞在同步队列,任何任务都会触发新的线程处理
使用场景:要求不会出现卡顿、响应及时的系统中
弊端:如果高并发场景下,服务器服务主线程提交任务速度高于线程池处理任务的速度,CachedThreadPool
将会不断的创建新的线程,在极端情况下, CachedThreadPool
会因为创建过多线程而耗尽CPU和内存资源
CachedThreadPool 的execute()方法的运行流程:
流程分析:
- 首先执行 SynchronousQueue.offer(Runnable task),添加一个任务。如果当前 CachedThreadPool中有空闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS), 其中NANOSECONDS是毫微秒即十亿分之一秒(就是微秒/1000),那么主线程执行offer操作与空闲线程执行poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则进入第2步;
- 当 CachedThreadPool初始线程数为空时,或者当前没有空闲线程,步骤1会失败,此时 CachedThreadPool会创建一个新的线程来执行任务, execute()方法执行完成;
- 根据构造参数我们知道指定空闲线程存活时间为60秒,如果步骤2创建的线程需要执行的任务完成后,会调用 SynchronousQueue.poll(),会让空闲线程在线程池中存活60秒,如果在此期间主线程提交了任务,则空闲线程会处理任务;否则,这个空闲线程会将会被终止。由于空闲60秒的线程会被终止,因此哪怕长时间没有任务处理的CachedThreadPool也不会占用系统资源。
总结:由于可能会无限创建新线程,所以实际开发中几乎不会使用这个线程池。
3、SingleThreadPool
SingleThreadPool 见名知意是创建一个线程数为1的线程池
创建SingleThreadPool
代码如下:
//通过Executors创建SingleThreadPool线程池
ExecutorService service = Executors.newSingleThreadExecutor();
SingleThreadPool
创建方法源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
源码分析:从静态方法可以看出参数corePoolSize和maximumPoolSize被设置为1,其余正常。
特点:线程池中指定只会创建1个线程,LinkedBlockingQueue
能够接收的最大任务数为Integer.MAX_VALUE
应用场景:由于只有一个线程,可以保证所有的任务按照先来后到顺序处理,可以用于处理需要保证顺序的任务。而且由于保证只有一个任务会被执行,还可以用来处理存在共享资源而不需要额外考虑加锁同步。
弊端:由于只有一个线程处理任务,如果有任务处理很慢,就会导致任务不断累积到LinkedBlockingQueue
无界队列中,最多可累积到Integer.MAX_VALUE数量的任务,导致服务响应很慢,客户端严重卡顿。
面试问题:既然
SingleThreadPool
是单线程的线程池,为什么不直接new一个线程处理呢?答:1、线程池是有任务队列的,如果new一个线程去处理,需要手动维护一个任务队列;
2、线程池是有一个完整的生命周期的,如果new一个线程去处理,需要自己维护线程的创建和销毁。
总结:各自的适用场景
- FixedThreadPool:适用于为了满足资源管理需求,而需要限制当前线程的数量的应用场景,它适用于负载比较重的服务器;
- CachedThreadPool:大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载较轻的服务器;
- SingleThreadExecutor:适用于需要保证执行顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的场景
六、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
继承自ThreadPoolExecutor
,并实现ScheduledExecutorService
接口。它主要用来完成在给定的延迟之后执行任务,或者专门用来执行定期任务。
1、ScheduledThreadPool
创建ScheduledThreadPool
代码如下:
//通过Executors创建ScheduledThreadPool线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
再来看一下ScheduledThreadPool
创建方法源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
可以发现此方法内部return一个ScheduledThreadPoolExecutor
,再来看一下其创建方法源码:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
源码分析:可以发现其核心线程数由传入参数决定,最大线程数为Integer.MAX_VALUE,重点关注其内部存放任务的阻塞队列是DelayedWorkQueue
。
特点:核心线程数自己决定,但是最大线程数为Integer.MAX_VALUE,内部存放任务的队列是延时队列DelayedWorkQueue
。
弊端:最大线程数为Integer.MAX_VALUE,如果使用不当,有可能导致无限创建服务线程,导致耗尽CPU系统资源。
应用场景:内部存放任务的阻塞队列为DelayedWorkQueue
,用于处理定时任务。
下面来看一下ScheduledThreadPool
的执行流程:
流程分析:
- 当调用
ScheduledThreadPoolExecutor
的scheduleAtFixedRate()
方法或者scheduleWithFixedDelay()
方法时,会向ScheduledThreadPoolExecutor
的DelayQueue
添加一个实现了RunnableScheduledFuture
接口的ScheduleFutureTask
; - 线程池中的线程从
DelayQueue
中获取ScheduleFutureTask
,然后执行任务。
使用示例:定时执行任务
public class T10_ScheduledPool {
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
//此方法功能:实现隔一定时间、以一定频率运行的任务
service.scheduleAtFixedRate(()->{
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}, 0, 500, TimeUnit.MILLISECONDS);//以500毫秒运行一次任务
}
}
关于处理定时任务,我们一般也很少使用ScheduledThreadPool
线程池,因为如果是简单的定时任务,直接使用Java自带的Timer;如果是复杂的定时任务,应该使用更加专业的定时框架如quartz
。所以对于ScheduledThreadPool
做到了解即可。
七、ForkJoinPool
使用场景:
- 分解、汇总的任务
- 用很少的线程可以执行很多的任务
- CPU密集型操作
1、ForkJoinPool
ForkJoinPool
的核心思想就是分治思想。
把大任务不断的切分(fork)成一定规模的小任务,小任务执行完的结果不断汇总(join),得到我们最终所求的结果。
创建ForkJoinPool
线程池对象:
ForkJoinPool fjp = new ForkJoinPool();
创建ForkJoinPool
线程池方法源码:
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
源码分析:创建ForkJoinPool
的时候,要求必须定义为指定的类型。原来的任务要求是Runnable
或者Callable
类型,ForkJoinPool
要求任务必须是能进行分治的任务,这个任务类型是ForkJoinTask
,不过由于ForkJoinTask
比较原始,现在一般使用其子类RecursiveAction
,观其名字我们就能知道这是一个支持递归过程的任务。
使用ForkJoinPool讲任务分治的示例:
public class TestForkJoinPool {
//长度为一百万的数组,里面数值都是通过Random随机产生的
//问题:要求对这个数组中所有的数进行数值的总和计算
static int[] nums = new int[1000000];
static Random r = new Random();
//使用ForkJoinPool将大任务拆分成的baseCase为五万长度
static final int MAX_NUM = 50000;
//单线程计算:通过stream流完成单线程的累加过程,耗时很长
static {
for(int i=0; i<nums.length; i++) {
nums[i] = r.nextInt(100);
}
System.out.println("单线程计算结果:" + Arrays.stream(nums).sum()); //stream api
}
//使用ForkJoinPool多线程处理任务
static class AddTaskRet extends RecursiveTask<Long> {
private static final long serialVersionUID = 1L;
int start, end;
AddTaskRet(int s, int e) {
start = s;
end = e;
}
@Override
protected Long compute() {
//如果数组长度低于50000,就进行数组元素累加求和
if(end-start <= MAX_NUM) {
long sum = 0L;
for(int i=start; i<end; i++) sum += nums[i];
return sum;
}
//如果数组长度唱过50000,就不断进行二分
int middle = start + (end-start)/2;
AddTaskRet subTask1 = new AddTaskRet(start, middle);
AddTaskRet subTask2 = new AddTaskRet(middle, end);
subTask1.fork();
subTask2.fork();
return subTask1.join() + subTask2.join();
}
}
public static void main(String[] args) throws IOException {
//创建ForkJoinPool
ForkJoinPool fjp = new ForkJoinPool();
//定义任务
AddTaskRet task = new AddTaskRet(0, nums.length);
//执行任务
fjp.execute(task);
//得到并输出结果,随机产生数值,故每次运行结果都不相同
//但是单线程和多线程获得的结果一定是相同的
long result = task.join();
System.out.println("多线程计算结果:" + result);
}
}
输出结果:
单线程计算结果:49517577
多线程计算结果:49517577
有关ForkJoinPool内部实现比较复杂,这里目前先略过。
2、WorkStealingPool
前面讲的基于ThreadPoolExecutor
创建的线程池都是有线程队列和任务队列,线程队列不断从任务队列里面获取任务并处理,而WorkStealingPool
线程池中的每一个线程都有各自的一个任务队列,当其中一个线程中的所有任务都处理完之后,它可以从别的线程的任务队列中获取任务并处理,可以帮助其他线程分担压力。
常规基于ThreadPoolExecutor
和WorkStealingPool
的对比:
WorkStealingPool
中的线程从别的线程中分担任务流程:
创建WorkStealingPool
线程池代码如下:
//通过Executors创建WorkStealingPool线程池
ExecutorService service = Executors.newWorkStealingPool();
创建过程:
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
源码分析:可以发现创建WorkStealingPool其实就是创建一个ForkJoinPool并返回,所以只要理解了ForkJoinPool的创建过程,其实也就理解了WorkStealingPool的创建过程。
补充:
1、线程池分类
2、阻塞队列
关于线程池的学习到此结束,由于笔者能力有限,目前大致只能做出以上总结,但是相信如果能理解以上总结的知识,应对常规的面试应该绰绰有余了。
有关多线程、并发编程的所有知识点目前都总结完了,短期之内应该不会再有更新了,相关系列文章在下面都有链接。如果对你有帮助,欢迎点赞、评论以及转发呦,转载只要标明出处即可。
关联文章:
多线程/并发编程——同步工具类(CountDownLatch、Semaphore、ReadWriteLock、CyclicBarrier )