Future和ExecutorCompletionService
1、线程池中采用submit方法执行
为什么要来记录一下这个方法呢,主要是我在测试代码中写了一个bug,代码如下所示:
public class FutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<String> submit = executorService.submit(() -> {
TimeUnit.SECONDS.sleep(10);
return "hello,world";
});
System.out.println("代码执行啦,准备获取得到结果");
String s = submit.get();
System.out.println(String.format("获取得到的结果是:%s",s));
}
}
主要是因为使用了submit方法之后,主线程想要获取得到执行的值,结果发现主线程一直阻塞在获取得到结果。
所以来探究一下具体的实现过程
submit源代码
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
可以看到submit的本质上还是调用了execute方法来执行任务。
这里直接创建了一个任务,那么有必要掰扯一下这个任务对象了。
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
看一下构造函数:
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
// 作为任务
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
看一下继承体系结构
之所以可以用execute方法来进行执行,是因为FutureTask也是一个Runnable接口。
那么看一下具体的执行代码:
因为这里是在线程池中来进行执行的,所以只看一下关键代码即可。最终需要调用对象的task.run();
执行FutureTask对象的run方法
public void run() {
// 看一下这里的thread设置的值
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 让线程执行并返回对应的结果
result = c.call();
// 如果任务执行结束,那么这里做个标记
ran = true;
} catch (Throwable ex) {
// 如果执行代码出现了异常,结果置空,并设置异常
result = null;
ran = false;
setException(ex);
}
// 如果任务正常完成,那么需要唤醒阻塞的线程
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
这里思考一下,为什么要唤醒阻塞的线程?线程为什么会阻塞?
因为主线程一直在获取得到结果,而对于获取不到结果的线程来说,会阻塞,那么是怎么阻塞的?
这个得看下get方法中的阻塞逻辑。
唤醒阻塞线程之set方法
那么看一下唤醒阻塞线程的set方法:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
// 最终调用结束方法
finishCompletion();
}
}
完成任务唤醒被阻塞线程finishCompletion方法
看下方法:
private void finishCompletion() {
// assert state > COMPLETING;
// 只有get方式的时候,才会执行到这一步,所以先去看下get方法
for (WaitNode q; (q = waiters) != null;) {
// 比较会置换这里的值
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
// 任务置空并唤醒阻塞的代码
q.thread = null;
LockSupport.unpark(t);
}
// 循环置换
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
done();
callable = null; // to reduce footprint
}
call方法执行中出现异常setException(ex)方法
可以看到正常执行和执行任务期间出现了问题调用方法对象不同:
protected void setException(Throwable t) {
// 异常也会设置完成标识
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
// 设置最终的状态为异常。
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
// 最终唤醒线程
finishCompletion();
}
}
get方法
上面是唤醒代码,那么下面就应该是阻塞代码了。
public V get() throws InterruptedException, ExecutionException {
int s = state;
// 等待代码。只有state为NORMAL以及NORMAL之后的才会获取到结果
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
那么如果任务还没有执行完成,那么就需要阻塞来得到结果。具体的阻塞代码就在awaitDone方法中
awaitDone方法
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// 判断超时代码
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 创建一个节点等待
WaitNode q = null;
boolean queued = false;
for (;;) {
// 如果是线程中断需要移除线程然后抛异常
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
// 这里有几种情况,如果真的成为了NORMAL,那么表示执行完成了
// 但是如果是异常等情况,那么再次判断q,看看是不是因为下面循环引起的
// q只有在第二轮循环之后才有值
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
// 第二轮循环中肯定是要将其置为COMPLETING状态了,那么这里将会造成调用get方法的线程让步执行
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
// 第一轮循环肯定是null,创建新的节点
else if (q == null)
q = new WaitNode();
// 在有一轮执行到这里,那么表示waiters是q
else if (!queued)
// 执行完成返回true
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
// 因为是false,所以会阻塞
// 状态是NEW的时候将会阻塞在这里
LockSupport.park(this);
}
}
report方法
private V report(int s) throws ExecutionException {
Object x = outcome;
// 正常执行并返回
if (s == NORMAL)
return (V)x;
// 因为中断引起的
if (s >= CANCELLED)
throw new CancellationException();
// 只有执行任务发生异常的才会执行到这里来。
throw new ExecutionException((Throwable)x);
}
2、问题
首先看下我的代码的打印输出:
pool-1-thread-2
pool-1-thread-1
submit最终的结果是:1
submit1最终的结果是:1
第一行先出来,但是隔了很久之后,后面三行瞬间出来,那么为什么会出现这样的效果呢?
我通过源码定位之后发现问题主要是在get方法这里:
Integer integer1 = submit.get();
Integer integer2 = submit1.get();
提交任务之后,创建FutureTask之后,就来进行执行,然后get方法异步等待结果。
但是因为第一行代码中的任务执行时间比较长,而get导致main线程一直在进行等待。而此时,第二个任务已经执行完成了(可以通过打印完成之间看看),但是得等到通知第二个get方法,但是第二个方法还没有阻塞住,也就是说还没有进入到循环中去。
所以等到第一个get方法执行结束之后,第二个方法才会来继续执行。
第一个代码任务执行时间比较长,而第二个get方法执行比较短。虽然第二个任务早已经执行完毕,但是仍然需要等待第一个任务执行结束。
那么这种方式使用起来感觉非常low,那么有什么好的解决方式呢?当然是有的,但是需要根据实际情况来进行考虑
如果说不一定要拿到任务执行的结果,那么可以考虑另外一个get超时API;如果说一定要拿到结果且保证异步执行,那么使用另外一个工具类
使用Callable和Future有没有产生新的线程?没有,这里只是作为任务来进行执行。
3、ExecutorCompletionService
利用ExecutorCompletionService来解决上面get阻塞时阻塞在调用者线程的问题。
那么看下对应的解决代码:
public class FutureTestPro {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,12,5, TimeUnit.SECONDS,new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
ExecutorCompletionService<Integer> completionService = new ExecutorCompletionService<>(threadPoolExecutor);
completionService.submit(()->myTask1());
completionService.submit(()->myTask2());
try {
Integer integer = completionService.take().get();
System.out.println(integer);
integer = completionService.take().get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
threadPoolExecutor.shutdownNow();
}
public static int myTask1(){
try {
Thread.sleep(10000);
System.out.println("task1----->>"+Thread.currentThread().getName());
return 1;
} catch (InterruptedException e) {
e.printStackTrace();
return 0;
}
}
public static int myTask2(){
try {
Thread.sleep(3000);
System.out.println("task2---->>>"+Thread.currentThread().getName());
return 1;
} catch (InterruptedException e) {
e.printStackTrace();
return 0;
}
}
}
控制台打印:
task2---->>>pool-1-thread-2
1
task1----->>pool-1-thread-1
1
因为任务2执行时间比较短,任务1执行时间比较长。所以这里先打印出来的是任务2.
实现原理
CompletionService接口的唯一实现类ExecutorCompletionService,主要是因为ExecutorCompletionService实现了Future中的done方法。
那么慢慢看对应的原理:
public class ExecutorCompletionService<V> implements CompletionService<V> {
private final Executor executor;
private final AbstractExecutorService aes;
// 主要是因为在这里添加了一个阻塞队列,将任务的结果保存起来,哪个限制性,哪个放在队头
private final BlockingQueue<Future<V>> completionQueue;
..........
}
先看下上面的使用过程中的操作:
public ExecutorCompletionService(Executor executor) {
if (executor == null)
throw new NullPointerException();
this.executor = executor;
// 传入的实现类要是AbstractExecutorService的子类,否则这个线程池就为null
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
// 默认的阻塞队列是LinkedBlockingQueue
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}
ExecutorCompletionService的submit方法
看下具体的方法:
public Future<V> submit(Callable<V> task) {
if (task == null) throw new NullPointerException();
// 看下重写的任务
RunnableFuture<V> f = newTaskFor(task);
executor.execute(new QueueingFuture(f));
return f;
}
重写任务:
private RunnableFuture<V> newTaskFor(Callable<V> task) {
if (aes == null)
// 如果线程池为空
return new FutureTask<V>(task);
else
// 线程池不为空的时候,也是上面的操作
return aes.newTaskFor(task);
}
看下对应的执行方法【重点】:
private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
// 真正核心的代码,在Future中的代码运行结束之后,这里会将先执行完成的任务添加到队列中来
protected void done() { completionQueue.add(task); }
private final Future<V> task;
}
利用多态的特性,最终执行的将会是这里的代码。
因为在Future中的finishCompletion方法中
private void finishCompletion() {
// 在通知的过程中说明已经有结果了,那么将结果保存起来
// 这里就是上面重写的目的
done();
callable = null; // to reduce footprint
}
将结果保存到阻塞队列中即可。
take方法
阻塞队列的take方法,这个是最为常见的了。
public Future<V> take() throws InterruptedException {
return completionQueue.take();
}
直接从阻塞队列中来进行获取得到对应的值。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-11-01 java基础之mapstruct