futuretask 的状态跃迁始于 run() 调用:先 cas 将 state 从 new 改为 completing,再执行 call() 并写入 outcome,最后更新为 normal 或 exceptional;completing 是保障结果原子写入的不可见中间态。

NEW 状态下调用 run() 才真正触发状态跃迁
很多人误以为 FutureTask 实例化后就“准备就绪”,其实 state == NEW 仅表示任务尚未被任何线程执行。真正启动状态流转的唯一入口是 run() 方法——它必须由某个线程显式调用(比如提交给 Executor,或手动 new Thread(task).start())。
关键点在于:run() 内部首先通过 CAS 尝试将 state 从 NEW 改为 COMPLETING;失败则说明已被其他线程抢先执行,直接 return。
- 若 CAS 成功,才进入实际计算逻辑(即
Callable.call()) - 此时任务已脱离“未启动”范畴,进入“正在完成结果”的中间态,但
outcome字段还没写入 -
COMPLETING是瞬时态,不可见于外部调用(isDone()返回true,但get()仍可能阻塞)
COMPLETING 是结果落盘前的临界保护窗口
COMPLETING 的存在不是为了暴露给用户,而是为原子性写入结果服务。在 run() 中,call() 执行完后,会先设置 outcome,再把 state 从 COMPLETING 更新为 NORMAL 或 EXCEPTIONAL。
这个两步更新(先 outcome,再 state)能避免竞态:多个线程同时调用 get() 时,只要看到 state >= COMPLETING,就会自旋等待 state 变成终态,而不会读到未初始化的 outcome。
-
COMPLETING期间,get()不返回、不抛异常,只等待 - 若此时调用
cancel(true),会失败(因为state已不是NEW),但可中断正在运行的call()线程 - 不能在
COMPLETING态调用runAndReset(),该方法只对NEW或终态有效
为什么不能跳过 COMPLETING 直接到 NORMAL
跳过中间态看似简化,实则破坏线程安全。如果 run() 直接 CAS state 到 NORMAL,再写 outcome,那么并发 get() 可能读到 state == NORMAL 却拿到未赋值的 outcome(JVM 指令重排序 + 缓存可见性问题)。
COMPLETING 强制引入一个“屏障”语义:只有 outcome 安全写入后,终态才对外可见。源码中这一步使用的是 UNSAFE.putOrderedInt,比 volatile 写更轻量,又足以保证后续读取 outcome 的正确性。
- 该设计牺牲了少量性能(多一次 CAS),换来了无锁读取结果的可靠性
- JDK 9+ 对此路径做了进一步优化,但
COMPLETING的语义角色没变 - 自定义子类若重写
done(),其回调时机就在COMPLETING → NORMAL/EXCEPTIONAL转换之后,此时outcome已确定
调试时如何观察 NEW → COMPLETING 这一跳
直接打印 state 值意义不大,因为 COMPLETING 太短暂。更实用的方式是结合断点和条件:
- 在
FutureTask.run()开头打断点,确认进入时state == NEW - 在
UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)后加日志或 watch 表达式state - 若想稳定复现
COMPLETING态,可在call()开头插入Thread.sleep(10),再另起线程调用get()并检查state - 注意:不要依赖
toString()输出的 state 名称,它只映射静态常量,不反映实时值
真正容易被忽略的是:这个状态跃迁完全由线程调度和 CAS 竞争决定,无法通过配置或参数干预。理解它,是为了在排查 get() 长时间阻塞时,能快速区分是卡在 NEW(任务根本没跑)、COMPLETING(计算中但结果未落盘),还是终态异常(ExecutionException 被吞)。











