背景
最近项目因为有个同事错误的使用了FutureTask,导致线程池没有真的进行异步操作,使得线上接口报超时,差点产生线上事故
首先看一下同事的代码
java
...业务代码,支付完成后执行操作
try {
Future<?> future = asyncTaskExecutor.submit(() -> this.afterPaySuccess(oilOrderDO.getOrderNo()));
// 打印线程池异常
future.get();
} catch (Exception e) {
LogUtil.error(log, "OrderService.updateOrder.asyncTaskExecutor >> 线程池异常 >> param = {}", param);
}
目的想必大家都看的出来,就是支付成功后,执行异步操作,比如推送,播报,状态更新等。线程池用过一段时间的朋友应该会发现,他这里拿到了Future,同时调用了Future.get,其实这样子是已经起不到异步的一个效果了,因为Future.get会阻塞线程,直到线程完成后,才会结束,这就会导致,用户支付后,无法直接返回结果,恰巧里面又有一个比较慢的方法,就导致了用户下单后,接口提示超时
源码解析
显然同事,是对这个方法理解错误了,认为他是返回线程池异常而不会阻塞线程,正所谓要知其所以然,虽然找到了原因,但还是要看一下为什么它可以阻塞线程。
先看一下submit方法,我们使用的是ThreadPoolTaskExecutor,本质是给ThreadPoolExecutor封装了一层
java
@Nullable
private ThreadPoolExecutor threadPoolExecutor;
public Future<?> submit(Runnable task) {
ExecutorService executor = this.getThreadPoolExecutor();
try {
return executor.submit(task);
} catch (RejectedExecutionException var4) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, var4);
}
}
ThreadPoolExecutor.submit实际上是有父类AbstractExecutorService实现,这里就构建了FeatureTask对象
java
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 生成FeatureTask对象
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
public FutureTask(Runnable runnable, V result) {
// 将runnable转成callable
this.callable = Executors.callable(runnable, result);
// 设置状态
this.state = NEW;
}
然后就是excute就不细讲了,需要的可以看这篇ThreadPoolExecutor源码解析 - 掘金 (juejin.cn),最终的结果肯定是执行callable里的run方法,然后获取返回值。但是这里都没有阻塞的步骤,所以我们先看future.get()方法
首先要清楚future也是有状态的
java
private volatile int state;
// 新建
private static final int NEW = 0;
// 完成
private static final int COMPLETING = 1;
// 正常
private static final int NORMAL = 2;
// 异常退出
private static final int EXCEPTIONAL = 3;
// 任务取消
private static final int CANCELLED = 4;
// 线程中断中
private static final int INTERRUPTING = 5;
// 线程已中断
private static final int INTERRUPTED = 6;
java
public V get() throws InterruptedException, ExecutionException {
// 获取当前状态
int s = state;
// 线程是NEW或者COMPLETING时等待
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
点开awaitDone方法
java
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;
// 执行完毕则,返回状态
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
// 运行中,则挂起
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
// 为空,则新建等待节点
else if (q == null)
q = new WaitNode();
// 新建的节点,放入等待队列
else if (!queued)
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
// 阻塞
LockSupport.park(this);
}
}
java
private V report(int s) throws ExecutionException {
Object x = outcome;
// 正常结束,返回callable结果
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
这个awaitDone方法看来就是阻塞线程的地方了,它会不停的监听等待队列中的节点,如果完成了,则返回对应的结果,如果未完成则继续等待
总结
当我们使用线程池时,需要考虑我们是否需要同步拿到线程的返回值,如果不需要则不用拿到future对象,否则会导致线程被阻塞,导致接口耗时增加,如果真的需要,建议带上超时时间,防止后续开发人员开发的时候,没有理解到位,将一些耗时很长的方法也放入其中,导致接口超时