本系列文章:
Java并发编程学习之路(一)并发编程三要素、Thread、Runnable、interrupted、join、sleep、yield
Java并发编程学习之路(二)线程同步机制、synchronized、CAS、volatile、final、Lock、AQS
Java并发编程学习之路(三)ReentrantLock、ReentrantReadWriteLock、死锁、原子类
Java并发编程学习之路(四)线程池、FutureTask
Java并发编程学习之路(五)线程协作、wait/notify/notifyAll、Condition、await/signal/signalAll、生产者–消费者
Java并发编程学习之路(六)ThreadLocal、BlockingQueue、CopyOnWriteArrayList、ConcurrentHashmap
Java并发编程学习之路(七)CountDownLatch、CyclicBarrier、Semaphore、Exchanger、Phaser
Java并发编程学习之路(八)多线程编程例子
前言 线程池简介
线程的开销主要包括以下几个方面:
- 1、线程的创建和启动的开销
与普通的对象相比,Java线程还占用了额外的存储空间-----栈空间。并且,线程的启动会产生相应的线程调度开销。 - 2、线程的销毁
- 3、线程调度的开销
线程的调度会导致上下文切换,从而增加处理器资源的消耗,使得应用程序本身可以使用的处理器资源减少。
一个系统能够创建的线程重视受限于该系统所拥有的处理器数目。无论是CPU密集型还是I/O密集型线程,这些线程的数量的临界值总是处理器的数目。
线程池图示:
线程池内部可以预先创建一定数量的工作者线程,客户端代码并不需要向线程池借用线程,而是将其执行的任务作为一个对象提交给线程池,线程池可能将这些任务缓存在工作队列之中,而线程池内部的各个工作者线程则不断地从队列中取出任务并执行
。因此,线程池可以被看作基于生产者—消费者模式的一种服务,该服务内部维护的工作者线程相当于消费者线程,线程池的客户端相当于生产者线程,客户端代码提交给线程池的任务相当于“产品”,线程池内部用于缓存任务的队列相当于传输通道。
线程池有什么优点?
- 1、降低资源消耗
重用存在的线程,减少对象创建销毁的开销
。 - 2、提高响应速度
可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
一、ThreadPoolExecutor
使用线程池管理线程主要有如下好处:
- 1、降低资源消耗
通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗; - 2、提升系统响应速度
通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度; - 3、提高线程的可管理性
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程。
1.1 线程池的创建
创建线程池主要是ThreadPoolExecutor类来完成,ThreadPoolExecutor的有许多构造方法,可以通过参数最多的构造方法来理解创建线程池有哪些需要配置的参数。参数最多的构造方法为:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- 1、corePoolSize:表示核心线程池的大小
线程池的基本大小,即在没有任务需要执行的时候线程池的大小。这里需要注意的是:
在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread或prestartAllCoreThreads事先启动核心线程。
- 2、maximumPoolSize:线程池中允许的最大线程数
线程池中的当前线程数目不会超过该值
。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。 - 3、keepAliveTime:空闲线程存活时间
如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话
,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。 - 4、unit:时间单位
为keepAliveTime指定时间单位,可以指定为:秒,毫秒,微秒,纳秒等。 - 5、workQueue:阻塞队列
保存任务的阻塞队列
。当调用execute方法时,如果线程池中没有空闲的可用线程,那么就会把这个Runnable对象放到该队列中。这个参数必须是一个实现BlockingQueue接口的阻塞队列,因为要保证线程安全。可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。 - 6、threadFactory:创建线程的工厂类
一般用默认即可,默认实现是Executors.defaultThreadFactory()。也可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字
,如果出现并发问题,也方便查找问题原因。 - 7、handler:饱和策略
当线程池的阻塞队列已满和指定的线程都已经运行,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:
- AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
- CallerRunsPolicy:只用调用者所在的线程来执行任务;
- DiscardPolicy:不处理直接丢弃掉任务;
- DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务。
RejectedExecutionHandler接口用于封装被拒绝的任务的处理策略,该接口中只有一个方法:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
r参数代表被拒绝的任务,e代表拒绝任务r的线程池实例。
上面说的4种拒绝策略,就对应了ThreadPoolExecutor中提供的4种RejectedExecutionHandler实现类:
RejectedExecutionHandler实现类 | 所实现的策略 |
---|---|
ThreadPoolExecutor.AbortPolicy | 直接抛出异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃当前被拒绝的任务,不抛出异常 |
ThreadPoolExecutor.DiscardOldestPolicy | 将工作队列中最久的任务丢弃,然后重新尝试被拒绝的任务 |
ThreadPoolExecutor.CallerRunsPolicy | 将被拒绝的任务返回给调用者处理 |
1.2 线程池工作原理
通过ThreadPoolExecutor创建线程池后,可以通过execute提交任务,execute方法源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果当前任务无法放进阻塞队列中,则执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
execute方法执行逻辑有这样几种情况:
如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务
;如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中
;如果当前workQueue队列已满并且线程个数未超过maximumPoolSize,则会创建新的线程来执行任务
;如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理
。
线程池的设计思想就是使用了核心线程池corePoolSize、阻塞队列workQueue和线程池线程最大个数maximumPoolSize所组成的缓存策略来处理任务。
从这个方法里,也就看出了线程池的工作原理:
线程池执行所提交的任务过程主要有这样几个阶段:
- 先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步;
- 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步;
- 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理。
到此,可以看个使用线程池的例子:
public class ThreadPoolTest{
public static void main(String[] args) {
// 创建一个可重用固定线程数的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
// 创建线程
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
// 将线程放入池中进行执行
threadPoolExecutor.execute(t1);
threadPoolExecutor.execute(t2);
threadPoolExecutor.execute(t3);
threadPoolExecutor.execute(t4);
threadPoolExecutor.execute(t5);
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
}
}
结果:
pool-1-thread-1正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-4正在执行。。。
pool-1-thread-5正在执行。。。
1.3 线程池的生命周期
- 1、RUNNING
线程池一旦被创建,就处于 RUNNING 状态
,任务数为 0,能够接收新任务,对已排队的任务进行处理。 - 2、SHUTDOWN
不接收新任务,但能处理已排队的任务
。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。 - 3、STOP
不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态
。 - 4、TIDYING:
所有的任务都已终止了,workerCount (有效线程数) 为0。进入TIDYING状态的两种方式:
SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态
。- STOP状态下,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
- 5、TERMINATED:
线程池彻底终止
。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
线程池的生命周期:
1.4 线程池的关闭
要关闭线程池,有shutdown和shutdownNow两个方法:
public void shutdown()
public List<Runnable> shutdownNow()
- 1、shutdown()
把线程池的状态设置成SHUTDOWN状态,然后中断所有没有正执行任务的线程。 - 2、shutdownNow()
首先把线程池的状态设置成STOP,然后尝试停止所有正在执行任务或者暂停任务的线程,并返回等待执行任务的列表。
要检测线程池中的状态,有以下几个方法:
- 1、isShutdown()
只要调用了shutdown或者shutdownNow,isShutdown就会返回true,否则false。 - 2、isTerminaed()
当所有任务关闭之后,才表示线程池关闭成功,这时会返回true。 - 3、isTerminating()
执行了shutdown或shutdownNow之后,还有任务正在进行中=true,没有任务进行中=false。
关于线程池的关闭,示例:
public class ThreadPoolTest{
public static void main(String[] args) {
// 创建一个可重用固定线程数的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
// 创建线程
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
// 将线程放入池中进行执行
threadPoolExecutor.execute(t1);
threadPoolExecutor.execute(t2);
threadPoolExecutor.execute(t3);
threadPoolExecutor.execute(t4);
threadPoolExecutor.execute(t5);
System.out.println(threadPoolExecutor.isShutdown());
threadPoolExecutor.shutdown();
System.out.println(threadPoolExecutor.isShutdown());
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
}
}
此程序的运行结果并不唯一,不过两次调用threadPoolExecutor.isShutdown()
结果是一样的,第一次是false,第二次是true。示例:
pool-1-thread-1正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-2正在执行。。。
false
pool-1-thread-4正在执行。。。
pool-1-thread-5正在执行。。。
true
1.5 线程池阻塞队列
阻塞队列的作用:用来存储等待执行的任务
。阻塞队列中的任务也FIFO特性的,即先入队,先执行。
常见阻塞队列有4种,此处先简单了解,后续有专门章节进行介绍。具体的队列:
- 1、ArrayBlockingQueue
一个用数组实现的有界阻塞队列
,按照先入先出(FIFO)
的原则对元素进行排序。可以设置是否采用公平策略,使用较少。 - 2、PriorityBlockingQueue
支持优先级的无界阻塞队列
,使用较少。 - 3、LinkedBlockingQueue
一个用链表实现的有界阻塞队列,队列默认和最长长度为Integer.MAX_VALUE
。队列按照先入先出的原则对元素进行排序,使用较多。吞吐量通常要高于 ArrayBlockingQueue
。 - 4、SynchronousQueue
不储存元素(无容量)的阻塞队列,每个put操作必须等待一个take操作,否则不能继续添加元素
。支持公平访问队列,常用于生产者,消费者模型,吞吐量较高,使用较多。
1.6 线程池的拒绝策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务,说明线程池处于饱和状态,此时必须采取一种策略处理新提交的任务,这种策略就是拒绝策略。
5种拒绝策略:
- 1、ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy,直接抛出RejectedExecutionException异常
。 - 2、ThreadPoolExecutor.CallerRunsPolicy
在任务被拒绝添加后,会用调用execute函数的上层线程,让调用者去处理。 - 3、ThreadPoolExecutor.DiscardPolicy
不处理新任务,直接丢弃
。不会抛异常也不会执行。 - 4、ThreadPoolExecutor.DiscardOldestPolicy
当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去重试
。 - 5、自定义拒绝策略
比如想让被拒绝的任务在一个新的线程中执行,示例:
static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
new Thread(r,"新线程"+new Random().nextInt(10)).start();
}
}
此处以AbortPolicy为例,看一下拒绝策略的效果:
public class ThreadPoolTest{
public static void main(String[] args) {
// 创建一个可重用固定线程数的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
// 创建线程
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
Thread t6 = new MyThread();
Thread t7 = new MyThread();
// 将线程放入池中进行执行
threadPoolExecutor.execute(t1);
threadPoolExecutor.execute(t2);
threadPoolExecutor.execute(t3);
threadPoolExecutor.execute(t4);
threadPoolExecutor.execute(t5);
threadPoolExecutor.execute(t6);
threadPoolExecutor.execute(t7);
threadPoolExecutor.shutdown();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
}
}
在上述代码中,线程池中最大线程数是5,阻塞队列数量是1,所以当第7个线程进入线程池时,如果前6个线程都在执行任务,那么就需要拒绝策略来处理第7个线程了。结果示例:
1.7 线程池参数配置
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:
- 任务的性质:
CPU密集型任务,IO密集型任务和混合型任务
。- 任务的优先级:高,中和低。
- 任务的执行时间:长,中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。示例:
- 1、CPU密集型任务
CPU密集型任务配置尽可能少的线程数量,如配置CPU个数+1的线程数的线程池。 - 2、IO密集型任务
IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如配置两倍CPU个数+1。
IO密集型任务另一种配置经验:线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间
。
例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式:4 * 100% * 100% / 50% = 8。
例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式:4 * 100% * 100% / 10% = 40。
混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务。设备的CPU个数可以通过Runtime.getRuntime().availableProcessors()
方法获得。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
同时,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃
。
1.8 线程池死锁
如果线程池中执行的任务在其执行过程中又会向同一个线程池提交另一个人任务,而前一个任务的执行结束又依赖后一个任务的执行结果,那么就有可能出现这样的情形:线程池中的所有工作者线程都处于等待其他任务的处理结果而这些任务仍在工作队列中等待执行,由于线程池中已经没有可以对工作队列中的任务进行处理的工作者线程,这种等待就会一直持续下去从而形成死锁。
因此,适合提交给同一线程池实例执行的任务是相互独立的任务,而不是彼此有依赖关系的任务。对于彼此存在依赖关系的任务,可以考虑使用不同的线程池实例来执行这些任务
。
示例:
public class ThreadPoolTest{
ExecutorService exec = Executors.newSingleThreadExecutor();
/**
* 该任务会提交另外一个任务到线程池,并且等待任务的执行结果
*/
public class RenderPageTask implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("RenderPageTask 依赖LoadFileTask任务返回的结果...");
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBody();
return header.get() + page + footer.get();
}
public String renderBody() {
return "render body is ok.";
}
}
public static void main(String[] args) {
ThreadPoolTest lock = new ThreadPoolTest();
Future<String> result = lock.exec.submit(lock.new RenderPageTask());
try {
System.out.println("last result:" + result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class LoadFileTask implements Callable<String> {
private String fileName;
public LoadFileTask(String fileName) {
this.fileName = fileName;
}
@Override
public String call() throws Exception {
System.out.println("LoadFileTask execute call...");
return fileName;
}
}
结果:
RenderPageTask 依赖LoadFileTask任务返回的结果…
例子中的线程池中只有一个线程,但是要先进入线程池的任务却要依赖后续任务的结果,就发生了死锁。
二、Executors
2.1 线程池相关类
- 1、Executor
线程池的顶级接口是Executor,Executor中只定义了一个线程执行的方法:
public interface Executor {
void execute(Runnable command);
}
- 2、ExecutorService
ExecutorService继承了Executor,扩展了线程池的接口:
public interface ExecutorService extends Executor {
//停止未执行的任务,执行完目前正在执行的任务
void shutdown();
//停止正在执行和待执行的任务,返回待执行任务列表
List<Runnable> shutdownNow();
//如果ExecutorService已关闭,则返回true。
boolean isShutdown();
//如果ExecutorService关闭后所有任务都已完成,则返回true。
boolean isTerminated();
//阻塞直到所有任务在一个关闭的请求后完成执行,或者超时发生,
//或者当前线程被中断,以先发生者为准。
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
//提交一个返回值的任务进行执行,并返回一个代表任务未决结果的Future。
//Future的get方法将在任务成功完成后返回任务的结果。
<T> Future<T> submit(Callable<T> task);
//提交Runnable任务以供执行并返回代表该任务的Future。
//Future的get方法将在任务成功完成后返回给定的result。
<T> Future<T> submit(Runnable task, T result);
//提交Runnable任务以供执行并返回代表该任务的Future。Future的get方法
//将在成功完成后返回null。
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;
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
- 3、ThreadPoolExecutor
ThreadPoolExecutor继承AbstractExecutorService,AbstractExecutorService实现ExecutorService接口。
ThreadPoolExecutor是用来创建线程池的主要类,Executors工具类创建线程池本质上也是通过ThreadPoolExecutor实现
。 - 4、ScheduledExecutorService
定义了执行周期性任务的接口:
public interface ScheduledExecutorService extends ExecutorService {
/**
* 带延迟时间的调度,只执行一次
* 调度之后可通过Future.get()阻塞直至任务执行完毕
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 带延迟时间的调度,只执行一次
* 调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
/**
* 带延迟时间的调度,循环执行,固定频率
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 带延迟时间的调度,循环执行,固定延迟
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
- 5、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承ThreadPoolExecutor,实现ScheduledExecutorService接口,是用来执行周期性任务调度的线程池。 - 6、Executors
线程池生成工具类,在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
Executors工厂类可以创建四种线程池,分别为:
newCachedThreadPool
:创建一个可缓存线程池
。如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,否则新建线程。(线程最大并发数不可控制)newFixedThreadPool
:创建一个固定大小
的线程池。可控制线程最大并发数,超出的线程会在队列中等待。newScheduledThreadPool
: 创建一个定时
线程池,支持定时及周期性任务执行。newSingleThreadExecutor
:创建一个单线程化
的线程池。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
2.2 四种线程池
上文提到,Executors工厂类可以创建四种线程池:newCachedThreadPool 、newFixedThreadPool、newScheduledThreadPool和newSingleThreadExecutor。
此处先创建一个统一的线程任务,方便测试四种线程池:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running...");
}
}
2.2.1 newFixedThreadPool
newFixedThreadPool的实现:
/**
* 核心线程池大小=最大线程池大小=传入参数
* 线程过期时间为0ms
* LinkedBlockingQueue作为工作队列
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newFixedThreadPool方法里的构造方法,其实调用了重载ThreadPoolExecutor方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
newFixedThreadPool的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列
。
此外 keepAliveTime 为 0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义)。
newFixedThreadPool选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE
,相当于没有上限。
newFixedThreadPool线程池执行任务的流程:
- 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务;
- 线程数等于核心线程数后,将任务加入阻塞队列;
- 由于队列容量非常大,可以一直加;
- 执行完任务的线程反复去队列中取任务执行。
FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。示例:
public class FixedThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
MyRunnable myRunnable = new MyRunnable();
for (int i = 0; i < 5; i++) {
executorService.execute(myRunnable);
}
System.out.println("线程任务开始执行");
executorService.shutdown();
}
}
结果:
线程任务开始执行
pool-1-thread-1 is running…
pool-1-thread-1 is running…
pool-1-thread-2 is running…
pool-1-thread-1 is running…
pool-1-thread-2 is running…
2.2.2 newSingleThreadExecutor
newSingleThreadExecutor的实现:
/**
* 核心线程池大小=最大线程池大小=1
* 线程过期时间为0ms
* LinkedBlockingQueue作为工作队列
*/
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
从参数可以看出来,SingleThreadExecutor相当于特殊的 FixedThreadPool,这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
SingleThreadExecutor的执行流程:
- 线程池中没有线程时,新建一个线程执行任务;
- 有一个线程以后,将任务加入阻塞队列,不停的加;
- 唯一的这一个线程不停地去队列里取任务执行。
SingleThreadExecutor 用于串行执行任务的场景
,每个任务必须按顺序执行,不需要并发执行。示例:
public class SingleThreadExecutorTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyRunnable myRunnable = new MyRunnable();
for (int i = 0; i < 5; i++) {
executorService.execute(myRunnable);
}
System.out.println("线程任务开始执行");
executorService.shutdown();
}
}
结果:
线程任务开始执行
pool-1-thread-1 is running…
pool-1-thread-1 is running…
pool-1-thread-1 is running…
pool-1-thread-1 is running…
pool-1-thread-1 is running…
2.2.3 newCachedThreadPool
newCachedThreadPool的实现:
/**
* 核心线程池大小=0
* 最大线程池大小为Integer.MAX_VALUE
* 线程过期时间为60s
* 使用SynchronousQueue作为工作队列
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可缓存的线程池
,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
newCachedThreadPool使用的队列是 SynchronousQueue,这个队列的作用就是传递任务,并不会保存
。
因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。
它的执行流程:
- 没有核心线程,直接向 SynchronousQueue 中提交任务;
- 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个;
- 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜;
- 由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。
CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
public class CachedThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
MyRunnable myRunnable = new MyRunnable();
for (int i = 0; i < 5; i++) {
executorService.execute(myRunnable);
}
System.out.println("线程任务开始执行");
executorService.shutdown();
}
}
结果:
线程任务开始执行
pool-1-thread-1 is running…
pool-1-thread-4 is running…
pool-1-thread-2 is running…
pool-1-thread-5 is running…
pool-1-thread-3 is running…
2.2.4 newScheduledThreadPool
newScheduledThreadPool的实现:
/**
* 核心线程池大小=传入参数
* 最大线程池大小为Integer.MAX_VALUE
* 线程过期时间为0ms
* DelayedWorkQueue作为工作队列
*/
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
大小无限(实际上有限,为Integer.MAX_VALUE)的线程池
。此线程池支持定时以及周期性执行任务的需求。
ScheduledThreadPoolExecutor 的执行流程:
- 添加一个任务;
- 线程池中的线程从 DelayQueue 中取任务;
- 然后执行任务。
具体执行任务的步骤也比较复杂:
- 线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask;
- 执行完后修改这个 task 的 time 为下次被执行的时间;
- 然后再把这个 task 放回队列中。
ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
public class ScheduledThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(3);
MyRunnable myRunnable = new MyRunnable();
for (int i = 0; i < 5; i++) {
// 参数1:目标对象,参数2:隔多长时间开始执行线程,参数3:执行周期,参数4:时间单位
scheduledExecutorService
.scheduleAtFixedRate(myRunnable, 1, 2, TimeUnit.SECONDS);
}
System.out.println("线程任务开始执行");
}
}
结果:
线程任务开始执行
pool-1-thread-1 is running…
pool-1-thread-2 is running…
pool-1-thread-1 is running…
pool-1-thread-3 is running…
pool-1-thread-2 is running…
pool-1-thread-1 is running…
pool-1-thread-3 is running…
pool-1-thread-2 is running…
pool-1-thread-1 is running…
pool-1-thread-3 is running…
2.3 Executors和ThreaPoolExecutor的区别
Executors 各个方法的缺点:
newFixedThreadPool 和 newSingleThreadExecutor: 主要问题是堆积的请求处理队列(队列最大值是 Integer.MAX_VALUE)可能会耗费非常大的内存
,甚至 OOM。newCachedThreadPool 和 newScheduledThreadPool: 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程
,甚至 OOM。
ThreaPoolExecutor:创建线程池方式只有一种,就是使用它的构造函数,参数自己指定。
2.4 两种提交任务的方法
ExecutorService 提供了两种提交任务的方法:
execute():提交不需要返回值的任务
submit():提交需要返回值的任务
- 1、execute
execute方法:
void execute(Runnable command);
execute() 的参数是一个 Runnable,也没有返回值。因此提交后无法判断该任务是否被线程池执行成功。示例:
ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
@Override
public void run() {
//do something
}
});
- 2、submit
submit方法源码:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
其中Callable接口中只定义了一个方法:
V call() throws Exception
Callable接口相当于一个增强型的Runnable接口,call方法的返回值代表相应任务的处理结果,而Runnable接口中的run方法既无返回值也不能抛出异常
。Executors.callable(Runnable task,T result)可以将Runnable接口转换为Callable接口实例。
submit() 有三种重载,参数可以是 Callable 也可以是 Runnable
。
同时它会返回一个 Funture 对象,通过它我们可以判断任务是否执行成功。
如果要获得执行结果,可以调用 Future.get() 方法,这个方法会阻塞当前线程直到任务完成。
提交一个 Callable 任务时,需要使用 FutureTask 包一层,示例:
FutureTask futureTask = new FutureTask(new Callable<String>() {
//创建 Callable 任务
@Override
public String call() throws Exception {
String result = "";
//do something
return result;
}
});
Future<?> future = executor.submit(futureTask); //提交到线程池
try {
Object result = future.get(); //获取结果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
- 3、execute和submit的区别
- 接收参数:
submit()可以执行 Runnable 和 Callable 类型的任务。execute()只能执行 Runnable 类型的任务
。- 返回值:
submit()方法可以返回持有计算结果的 Future 对象
,而execute()没有。- 异常处理:
submit()方便Exception处理
。
2.5 Executors和ThreaPoolExecutor创建线程池的区别
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让开发者更加明确线程池的运行规则,规避资源耗尽的风险
。
Executors 各个方法的弊端:
newFixedThreadPool 和 newSingleThreadExecutor,主要问题是堆积的请求处理队列(队列最大值是Integer.MAX_VALUE)可能会耗费非常大的内存
,甚至 OOM。newCachedThreadPool 和 newScheduledThreadPool。主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程
,甚至 OOM。
ThreaPoolExecutor创建线程池方式只有一种,就是使用它的构造函数,参数由开发者自己指定。
三、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor可以用来在给定延时后执行异步任务或者周期性执行任务,相对于任务调度的Timer来说,其功能更加强大,Timer只能使用一个后台线程执行任务,而ScheduledThreadPoolExecutor则可以通过构造函数来指定后台线程的个数。ScheduledThreadPoolExecutor类的继承关系:
从继承关系上看ScheduledThreadPoolExecutor的特点:
- 继承了ThreadPoolExecutor,拥有execute()和submit()提交异步任务的基础功能;
- 实现了ScheduledExecutorService接口,能够延时执行任务和周期执行任务。
3.1 构造方法
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), handler);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
由于ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,它的构造方法实际上是调用了ThreadPoolExecutor。
3.2 特有方法
ScheduledThreadPoolExecutor实现了ScheduledExecutorService接口,该接口定义了可延时执行异步任务和可周期执行异步任务的特有功能,相应的方法分别为:
//达到给定的延时时间后,执行任务。这里传入的是实现Runnable接口的任务,
//因此通过ScheduledFuture.get()获取结果为null
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
//达到给定的延时时间后,执行任务。这里传入的是实现Callable接口的任务,
//因此,返回的是任务的最终计算结果
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
//是以上一个任务开始的时间计时,period时间过去后,检测上一个任务是否执行
//完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执
//行完毕,则需要等上一个任务执行完毕后立即执行
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
//当达到延时时间initialDelay后,任务开始执行。上一个任务执行结束后到下一次
//任务执行,中间延时时间间隔为delay。以这种方式,周期性执行任务。
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
3.3 可周期性执行的任务
ScheduledThreadPoolExecutor最大的特色是能够周期性执行异步任务,当调用schedule,scheduleAtFixedRate和scheduleWithFixedDelay方法时,实际上是将提交的任务转换成的ScheduledFutureTask类。以schedule方法为例:
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
通过decorateTask会将传入的Runnable转换成ScheduledFutureTask类。线程池最大作用是将任务和线程进行解耦,线程主要是任务的执行者,任务也就是现在所说的ScheduledFutureTask。紧接着,会想到任何线程执行任务,总会调用run()方法。为了保证ScheduledThreadPoolExecutor能够延时执行任务以及能够周期性执行任务,ScheduledFutureTask重写了run方法:
public void run() {
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
//如果不是周期性执行任务,则直接调用run方法
ScheduledFutureTask.super.run();
//如果是周期性执行任务的话,需要重设下一次执行任务的时间
else if (ScheduledFutureTask.super.runAndReset()) {
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
在重写的run方法中会先if (!periodic)判断当前任务是否是周期性任务,如果不是的话就直接调用run()方法;否则的话执行setNextRunTime()方法重设下一次任务执行的时间,并通过reExecutePeriodic(outerTask)方法将下一次待执行的任务放置到DelayedWorkQueue中。
因此,可以得出结论:ScheduledFutureTask最主要的功能是根据当前任务是否具有周期性,对异步任务进行进一步封装。如果不是周期性任务(调用schedule方法)则直接通过run()执行,若是周期性任务,则需要在每一次执行完后,重设下一次执行的时间,然后将下一次任务继续放入到阻塞队列中。
3.4 DelayedWorkQueue
在ScheduledThreadPoolExecutor中还有另外的一个重要的类就是DelayedWorkQueue。为了实现其ScheduledThreadPoolExecutor能够延时执行异步任务以及能够周期执行任务,DelayedWorkQueue进行相应的封装。DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面。
定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。DelayedWorkQueue是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。
DelayedWorkQueue的部分源码:
//初始大小
private static final int INITIAL_CAPACITY = 16;
//DelayedWorkQueue是由一个大小为16的数组组成,数组元素为实现RunnableScheduleFuture接口的类
//实际上为ScheduledFutureTask
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
private int size = 0;
关于DelayedWorkQueue我们可以得出这样的结论:DelayedWorkQueue是基于堆的数据结构,按照时间顺序将每个任务进行排序,将待执行时间越近的任务放在在队列的队头位置,以便于最先进行执行。
3.5 ScheduledThreadPoolExecutor执行过程
现在我们对ScheduledThreadPoolExecutor的两个内部类ScheduledFutueTask和DelayedWorkQueue进行了了解,实际上这也是线程池工作流程中最重要的两个关键因素:任务以及阻塞队列。现在我们来看下ScheduledThreadPoolExecutor提交一个任务后,整体的执行过程。以ScheduledThreadPoolExecutor的schedule方法为例,具体源码为:
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
//将提交的任务转换成ScheduledFutureTask
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
//延时执行任务ScheduledFutureTask
delayedExecute(t);
return t;
}
方法很容易理解,为了满足ScheduledThreadPoolExecutor能够延时执行任务和能周期执行任务的特性,会先将实现Runnable接口的类转换成ScheduledFutureTask。然后会调用delayedExecute方法进行执行任务,这个方法也是关键方法,来看下源码:
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
//如果当前线程池已经关闭,则拒绝任务
reject(task);
else {
//将任务放入阻塞队列中
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
//保证至少有一个线程启动,即使corePoolSize=0
ensurePrestart();
}
}
该方法的重要逻辑会是在ensurePrestart()方法中,它的源码为:
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
关键在于它所调用的addWorker方法,该方法主要功能:新建Worker类,当执行任务时,就会调用被Worker所重写的run方法,进而会继续执行runWorker方法。在runWorker方法中会调用getTask方法从阻塞队列中不断的去获取任务进行执行,直到从阻塞队列中获取的任务为null的话,线程结束终止。
3.6 ScheduledThreadPoolExecutor使用
ScheduledThreadPoolExecutor使用示例:
public class ScheduledThreadPoolExecutorTest implements Runnable {
private ScheduledExecutorService scheduledExecutorService;
private void showTime() {
scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
System.out.println(Thread.currentThread().getName() + ": " + new Date());
scheduledExecutorService.schedule(new ScheduledThreadPoolExecutorTest(), 10, TimeUnit.SECONDS);
scheduledExecutorService.shutdown();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": " + new Date());
}
public static void main(String[] args) {
new ScheduledThreadPoolExecutorTest().showTime();
}
}
结果示例:
main: Mon Oct 18 21:24:13 CST 2021
pool-1-thread-1: Mon Oct 18 21:24:23 CST 2021
四、FutureTask
Future接口和实现Future接口的FutureTask类,代表异步计算的结果。
- 同步任务
以同步方式执行的任务,任务的发起和任务的执行是串行的。同步任务就像打电话:先拨打对方的号码(任务的发起),只有在电话接通(任务开始执行)之后,才能将消息告诉对方(任务执行的过程)。 - 异步任务
以异步方式执行的任务,任务的发起和任务的执行是并发的。异步任务就像发短信:只要给对方发一条短信(任务的发起),便认为通知到对方了,而不必关心对方什么时候看这条短信(任务)开始执行。
4.1 Future
Future是异步计算结果的顶级接口,定义了一些用于异步计算的方法:
public interface Future<V> {
//取消对任务的执行。如果任务已经启动,则mayInterruptIfRunning参数确定是否
//执行此任务的线程应该以试图停止任务被中断
boolean cancel(boolean mayInterruptIfRunning);
//如果任务在完成前被取消,将返回 true
boolean isCancelled();
//在任务完成、任务取消、任务异常的情况下都返回 true
boolean isDone();
//调用此方法会在获取计算结果前等待
V get() throws InterruptedException, ExecutionException;
//在单位时间内没有返回任务计算结果将超时,任务将立即结束
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
4.2 FutureTask介绍
FutureTask可以交给Executor执行,也可以由调用线程直接执行(FutureTask.run())。根据FutureTask.run()方法被执行的时机,FutureTask可以处于下面3种状态:
- 1、未启动
FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一个FutureTask,且没有执行FutureTask.run()方法之前,这个FutureTask处于未启动状态。 - 2、已启动
FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。 - 3、已完成
FutureTask.run()方法执行完后正常结束,或被取消,或执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态。
FutureTask状态装换图示:
FutureTask的状态有以上三种,在不同状态时结果也不同。
- 执行get方法时,可能的结果:
- 当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞;
- 当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或抛出异常;
- 执行cancel方法时,可能的结果:
- 当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将导致此任务永远不会被执行;
- 当FutureTask处于已启动状态时,执行FutureTask.cancel(true)方法将以中断执行此任务线程的方式来试图停止任务;
- 当FutureTask处于已启动状态时,执行FutureTask.cancel(false)方法将不会对正在执行此任务的线程产生影响(让正在执行的任务运行完成);
- 当FutureTask处于已完成状态时,执行FutureTask.cancel(…)方法将返回false。
FutureTask的get和cancel的执行图示:
4.3 FutureTask使用
FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务。示例:
public class FutureTaskTest1 {
public static void main(String[] args) {
FutureTaskTest1 futureTaskTest1 = new FutureTaskTest1();
// 创建任务集合
List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
// 传入Callable对象创建FutureTask对象
FutureTask<Integer> ft = new FutureTask<Integer>(futureTaskTest1.new ComputeTask(i, ""+i));
taskList.add(ft);
// 提交给线程池执行任务,也可以通过exec.invokeAll(taskList)一次性提交所有任务;
threadPool.submit(ft);
}
System.out.println("所有计算任务提交完毕, 主线程暂时不关心计算结果");
// 开始统计各计算线程计算结果
Integer totalResult = 0;
for(FutureTask<Integer> ft : taskList){
try {
//FutureTask的get方法会自动阻塞,直到获取计算结果为止
totalResult = totalResult + ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 关闭线程池
threadPool.shutdown();
System.out.println("多任务计算后的总结果是:" + totalResult);
}
private class ComputeTask implements Callable<Integer> {
private Integer result = 0;
private String taskName = "";
public ComputeTask(Integer iniResult, String taskName){
result = iniResult;
this.taskName = taskName;
System.out.println("生成子线程计算任务: "+taskName);
}
@Override
public Integer call() throws Exception {
for (int i = 0; i < 100; i++) {
result =+ i;
}
// 休眠5秒钟,观察主线程行为,预期的结果是主线程会继续执行,到要取得FutureTask的结果是等待直至完成。
Thread.sleep(5000);
System.out.println("子线程计算任务: "+taskName+" 执行完成!");
return result;
}
}
}
结果示例:
生成子线程计算任务: 0
生成子线程计算任务: 1
生成子线程计算任务: 2
生成子线程计算任务: 3
生成子线程计算任务: 4
生成子线程计算任务: 5
生成子线程计算任务: 6
生成子线程计算任务: 7
生成子线程计算任务: 8
生成子线程计算任务: 9
所有计算任务提交完毕, 主线程暂时不关心计算结果
子线程计算任务: 1 执行完成!
子线程计算任务: 0 执行完成!
子线程计算任务: 3 执行完成!
子线程计算任务: 4 执行完成!
子线程计算任务: 2 执行完成!
子线程计算任务: 5 执行完成!
子线程计算任务: 6 执行完成!
子线程计算任务: 7 执行完成!
子线程计算任务: 8 执行完成!
子线程计算任务: 9 执行完成!
多任务计算后的总结果是:990
4.4 runnable和callable有什么区别
Callable类似于Runnable,两者都可以执行任务。
Future接口表示异步任务,是一个可能还没有完成的异步任务的结果。Callable用于产生结果,Future 用于获取结果
。
- runnable和callable的相同点:
- 都是接口;
- 都可以编写多线程程序;
- 都采用Thread.start()启动线程。
- runnable和callable的主要区别:
- Runnable 接口 run 方法无返回值;
Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
;- Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;
Callable 接口 call 方法允许抛出异常,可以获取异常信息
;
Callalbe接口支持返回执行结果,需要调用FutureTask.get(),此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞
。
4.5 什么是FutureTask
FutureTask 表示一个异步运算
的任务。FutureTask里面可以传入一个 Callable 的具体实现类,通过FutureTask可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作
。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。
一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以 FutureTask 也可以放入线程池中。
示例:
public static void main(String[] args) throws Exception {
Callable<Integer> call = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println(new Date() + " 正在计算结果...");
Thread.sleep(3000);
return 1;
}
};
FutureTask<Integer> task = new FutureTask<>(call);
Thread thread = new Thread(task);
thread.start();
System.out.println(new Date() + " 干点别的...");
Integer result = task.get();
System.out.println(new Date() + " 拿到的结果为:" + result);
}
结果示例:
Tue Oct 19 22:10:46 CST 2021 干点别的…
Tue Oct 19 22:10:46 CST 2021 正在计算结果…
Tue Oct 19 22:10:49 CST 2021 拿到的结果为:1