原创

FutureTask.done 可能拖住谁:正常完成、取消与回调失败的三条路径

任务结果已经能get,线程池工作者却迟迟没有接下一项任务。排查Callable耗时很短,真正卡住的是FutureTask子类的done回调:它在完成路径里同步执行,业务函数返回以后,工作线程还没有从run退出。

更容易漏掉的是取消。done并不固定属于“执行任务的线程”。在本文验证的OpenJDK17完成路径和Corretto17.0.15运行中,正常完成由执行线程进入回调,成功取消则可以由调用cancel的线程进入。把数据库查询、远端通知或长时间清理放进去,可能同时拖住工作者与取消调用方。

让回调暂停,观察结果是否已经可用

以下程序不依赖线程池调度,直接给线程命名。正常回调被门闩暂停,主线程读取结果;另一项任务还没run就取消,观察回调线程;最后故意让done抛异常,核对它与Callable结果是否属于同一个失败通道。

保存为DoneThread.java,使用Java 17执行java DoneThread.java。这些输出是当前实现的可复现实验,不把API文档未承诺的线程归属扩大成所有自定义Future实现的规定。

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class DoneThread {
    static final class Observed extends FutureTask<String> {
        final CountDownLatch entered=new CountDownLatch(1),release;
        volatile String callbackThread;
        volatile boolean callbackFinished;
        Observed(Callable<String> body,boolean pause) {super(body);release=new CountDownLatch(pause?1:0);}
        protected void done() {
            callbackThread=Thread.currentThread().getName();entered.countDown();
            try {
                if(!release.await(3,TimeUnit.SECONDS))throw new IllegalStateException("callback gate timeout");
            }catch(InterruptedException e){Thread.currentThread().interrupt();throw new RuntimeException(e);}
            callbackFinished=true;
        }
    }
    static void join(Thread thread)throws InterruptedException {
        thread.join(3000);
        if(thread.isAlive()){thread.interrupt();throw new IllegalStateException("thread active: "+thread.getName());}
    }
    public static void main(String[] args)throws Exception {
        Observed normal=new Observed(()->"ok",true);
        Thread runner=new Thread(normal,"normal-worker");runner.start();
        try {
            if(!normal.entered.await(3,TimeUnit.SECONDS))throw new IllegalStateException("callback absent");
            System.out.println("normal callback thread="+normal.callbackThread);
            System.out.println("get="+normal.get(1,TimeUnit.SECONDS)+", callback finished="+normal.callbackFinished);
            System.out.println("worker still occupied="+runner.isAlive());
        }finally{normal.release.countDown();join(runner);}
        AtomicBoolean ran=new AtomicBoolean();
        Observed never=new Observed(()->{ran.set(true);return "unexpected";},false);
        Thread canceler=new Thread(()->never.cancel(false),"cancel-caller");canceler.start();join(canceler);
        never.run();
        System.out.println("cancel callback thread="+never.callbackThread+", body ran="+ran.get());
        System.out.println("cancelled="+never.isCancelled());
        AtomicReference<Throwable> uncaught=new AtomicReference<>();
        FutureTask<String> failingCallback=new FutureTask<>(()->"computed") {
            protected void done(){throw new IllegalStateException("callback failed");}
        };
        Thread failingRunner=new Thread(failingCallback,"failing-callback-worker");
        failingRunner.setUncaughtExceptionHandler((thread,error)->uncaught.set(error));
        failingRunner.start();join(failingRunner);
        System.out.println("result after callback failure="+failingCallback.get(1,TimeUnit.SECONDS));
        System.out.println("uncaught callback="+uncaught.get().getMessage());
        if(!"normal-worker".equals(normal.callbackThread)||!"cancel-caller".equals(never.callbackThread)||ran.get())
            throw new AssertionError("callback execution path");
    }
}
normal callback thread=normal-worker
get=ok, callback finished=false
worker still occupied=true
cancel callback thread=cancel-caller, body ran=false
cancelled=true
result after callback failure=computed
uncaught callback=callback failed

normal-worker还在done里等待,get已经返回ok,callbackFinished仍为false。这说明“计算结果可取得”与“该实现的全部后续回调都执行完”并不等价。若调用方拿到结果后需要等待额外收尾,就应有另一个完成协议,而不是猜done一定先结束。

cancel-caller取消从未启动的任务,同样触发done;随后调用never.run不会执行Callable。回调不能假设任务主体已经打开过文件或建立过连接,否则取消前启动路径就可能访问不存在的资源。需要清理时,应按实际资源状态判断所有权,不能只因为回调名字叫done就调用所有close。

最后一项Callable正常返回computed,done抛出的callback failed由线程的未捕获异常处理器看到,Future的get仍然取得计算结果。把重要的结果提交放在done,然后只靠get捕获ExecutionException,可能完全漏掉这条失败路径。

回调属于完成机制,不是新的后台执行器

FutureTask.done是受保护扩展点,不会因为覆写它就自动切到另一个线程池。正常完成路径同步调用它,成功取消路径也需要结束对应完成逻辑;在这些路径里执行阻塞工作,就把阻塞成本加给当前调用者。

如果确实要在完成后执行异步通知,可以向明确拥有生命周期和容量限制的执行器提交独立工作,并对提交拒绝与通知失败单独记录。不能把回调里“再提交一下”当作必定成功,否则线程池关闭或容量耗尽时,业务结果正常、通知却静默消失。

也不要为了规避回调耗时,直接新建无上限线程。大量任务一起完成时,原本受控的计算并发可能转成无界通知并发。更合理的是先决定通知是否必须可靠、是否需要持久任务记录,再选择恰当的处理链路;done本身没有提供可靠交付队列。

文档规定done在进入完成状态时被调用,正常、异常与取消都可能到达这里。代码需要区分isCancelled和get所表达的结果;对已经完成的Future调用get通常不会等待计算,但回调仍不该由此承担复杂的异常映射或外部事务。

取消状态不代表任务主体已经停止

本文使用取消前启动路径,把“哪个线程触发回调”展示得最清楚。运行中的任务被cancel(true)时,执行体可能尚未响应中断,Future却已经进入取消状态并运行相应完成回调。此时清理与仍在使用资源的任务主体可能发生并发,不能先close它的资源再假设它肯定不再访问。

因此让任务主体在自己的try/finally里释放执行期资源,通常更符合所有权;done可以完成短小的状态记录或发布结果,但不应拿“已取消”替代执行体结束确认。需要等待真正退出时,可采用任务自己的结束信号或执行器终止协议。

重写done以后,还要对回调自身失败作明确处理。样例故意抛异常并捕获到线程级处理器,是为了证明它不会自动变成Callable的结果失败。真实回调可以把诊断交给受控错误记录,但不能捕获一切后装作必须提交的业务动作已经完成。

回调顺序也应限制在当前实现能够说明的范围内。FutureTask可以被其他类继承、run和取消也可能被包装,不能从某次线程名推导所有完成监听器都遵守相同调度。库封装如果需要固定执行器,应把它写进自己的合同并验证两条完成路径。

这个实验保存了三项独立证据:正常回调仍占用工作者时结果已可取得、取消前启动时回调由取消者执行、回调异常没有改写计算结果。它们比只量一次Callable耗时更能解释“任务已经完成,线程却还忙”的现场,也能在以后往done里增加一段看似无害的远端调用时及时发现责任边界改变。

正文到此结束
评论插件初始化中...
Loading...