前言
在实际生产中,对线程池肯定不陌生,线程池的作用就是可以高效利用多核CPU的并行能力,将程序从串行执行,转变为并发执行。 其中线程池的一个典型的应用场景:提交一批任务需要并行执行,然后异步等待结果。结果的获取通常实现是利用Future机制,返回每个任务的Future,然后轮询阻塞等待执行结果。但是在一些任务场景下,这种是存在一定问题的
问题和描述
当我并发执行的任务的执行完成时间是不确定的,如果耗时特别久的服务在整个轮询的中间或者靠前为止,将会极大的影响整个流程的执行时间。举一个具体的例子:
- 当我服务A,要调用5个依赖服务:B,C,D,E,F,他们之间是没有相互依赖关系。
- 比如某次请求,外部依赖服务耗时(B=100,C=1000,D=10,E=100,F=12)
- 那整个异步处理结果的耗时至少会变为1000ms,因为其他任务结果的回调处理本来可以在C的等待中间处理完成
对于非在线接口来说,这种耗时是能够接受的。但是对于RT响应特别敏感的服务来说,这样的波动会比较致命的。
所以会存在一种需求:我提交一组任务,按照完成时间的排序将结果返回给我。业务方根据业务需求处理结果。在这种背景下:ExecutorCompletionService就粉墨登场了
ExecutorCompletionService 源码解析
主要功能说明
ExecutorCompletionService 内部维护了一个阻塞双端队列,用来保存线程池中已经完成的结果,通过调用它的take方法或poll方法可以获取到一个已经执行完成的Future,进而通过调用Future接口实现类的get方法获取最终的结果。
依赖关系
- Executor用于执行提交的任务
- BlockingQueue 阻塞队列,用于保存执行的结果
- QueueingFuture 内部类,用于包装整个执行的任务
那么就有一个疑问,整个流程到底是如何串联起来的,核心的点就在于QueueingFuture,后面会详细解析
源码解析
构造器
public ExecutorCompletionService(Executor executor) {
if (executor == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}
- Executor线程池由外部调用方提供,可以省去暴露更多线程池相关参数。完全由使用方自行解决
- LinkedBlockingQueue双端阻塞队列
任务提交
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;
}
将Callable包装为一个RunnableFuture,从上面的uml也可以看出,RunnableFuture也是实现了Runnable接口的,因此肯定是做了一定的封装。来看一下FutureTask的run()函数
任务执行
任务的执行是在线程池中,也就是RunnableFuture中的run函数
public void run() {
//该状态表示任务已经执行过了
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 = null;
int s = state;
if (s >= INTERRUPTING)
//主要处理中断
handlePossibleCancellationInterrupt(s);
}
}
从上面的代码中可以看到,在run中,对call做了一层封装,增加了执行结果的包装。来看一下set(result)
结果提交
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() {
//这里主要通知等待结果的线程,可以获取了
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
}
再看一下done():
protected void done() { }
该放回默认是空,但是QueueingFuture覆盖了该方法
protected void done() { completionQueue.add(task); }
在这里将结果提交到队列中
结果的获取
主要是take,poll方法
public Future<V> take() throws InterruptedException {
return completionQueue.take();
}
public Future<V> poll() {
return completionQueue.poll();
}
public Future<V> poll(long timeout, TimeUnit unit)
throws InterruptedException {
return completionQueue.poll(timeout, unit);
}
都在等待结果的返回。依托于阻塞队列
应用场景
需求:
通常在并行执行的任务中,业务调用方希望存在一个最长的等待时间或者成功比例,来确保对主流程造成重大影响。否则依照木桶短板理论,整个服务的稳定性必须依靠耗时最久的任务。在超时和80%结果返回,有些场景还是希望能够获得80%的数据
代码实现
public void test() throws InterruptedException, ExecutionException {
int coreSize = 10;
int maxSize = 100;
int queueSize = 1024;
ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, 60 * 1000L, TimeUnit.SECONDS,
new LinkedBlockingQueue(queueSize),
(new ThreadFactoryBuilder()).setNameFormat("test-thread").build(),
new ThreadPoolExecutor.DiscardPolicy());
ExecutorCompletionService<Integer> executorService = new ExecutorCompletionService<Integer>(executor);
List<Callable> taskList = Lists.newArrayList();
for (int index = 0; index < 9; index++) {
taskList.add(new Callable() {
@Override
public Object call() throws Exception {
int num = ThreadLocalRandom.current().nextInt(10, 100);
Thread.sleep(num);
return num;
}
});
}
taskList.add(new Callable() {
@Override
public Object call() throws Exception {
int num = 1000;
Thread.sleep(num);
return num;
}
});
taskList.stream().forEach(task -> executorService.submit(task));
// 获取结果,按照最长耗时
int waitTime = 300;
int leftTime = waitTime;
// 等待间隔时间
int inteval = 10;
int processCount = 0;
long statTime = System.currentTimeMillis();
while (leftTime > 0 && processCount != taskList.size()) {
long startTime = System.currentTimeMillis();
Future<Integer> future = executorService.poll(inteval, TimeUnit.MILLISECONDS);
leftTime -= (System.currentTimeMillis() - startTime);
if (Objects.nonNull(future)) {
processCount++;
System.out.println(future.get());
}
}
System.out.println("cost:" + (System.currentTimeMillis() - statTime));
}
上述的代码就是基于最大容忍度的结果返回:当耗时超过300ms直接返回已有的结果。也可以修改调整为根据一定返回比例+时间的方式区完成 运行结果: 整个结果是跟完成时间直接挂钩,且超过300后,直接丢弃后面的任务
后记
任何计数都有最适配的业务场景,广度和深度都很重要