源码|使用FutureTask的正确姿势

线程池的实现核心之一是FutureTask。在提交任务时,用户实现的Callable实例task会被包装为FutureTask实例ftask;提交后任务异步执行,无需用户关心;当用户需要时,再调用FutureTask#get()获取结果——或异常。

随之而来的问题是,如何优雅的获取ftask的结果并处理异常?本文讨论使用FutureTask的正确姿势。

JDK版本:oracle java 1.8.0_102

今天换个风格。

源码分析

从提交一个Callable实例task开始。

submit()

ThreadPoolExecutor直接继承AbstractExecutorService的实现。

public abstract class AbstractExecutorService implements ExecutorService {
...
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
...
}

后续流程可参考源码|从串行线程封闭到对象池、线程池。最终会在ThreadPoolExecutor#runWorker()中执行task.run()。

task即5行创建的ftask,看newTaskFor()。

newTaskFor()

AbstractExecutorService#newTaskFor()创建一个RunnableFuture类型的FutureTask。

public abstract class AbstractExecutorService implements ExecutorService {
...
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }
...
}

看FutureTask的实现。

FutureTask

public class FutureTask<V> implements RunnableFuture<V> {
...
    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;
...
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
...
}

构造方法的重点是初始化ftask状态为NEW。

状态机

状态转换比较少,直接给状态序列:

* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED

状态在后面有用.

run()

简化如下:

public class FutureTask<V> implements RunnableFuture<V> {
...
    public void run() {
        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);
        }
    }
...
}

如果执行时未抛出异常

如果未抛出异常,则ran==true,FutureTask#set()设置结果。

public class FutureTask<V> implements RunnableFuture<V> {
...
    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
...
}
  • outcome中保存结果result
  • 连续两步设置状态到NORMAL
  • finishCompletion()执行一些清理

记住outcome。

相当于4行获取独占锁,5-6行执行锁中的操作(注意,7行是不加锁的)。

如果执行时抛出了异常

如果运行时抛出了异常,则被12行catch捕获,FutureTask#setException()设置结果;同时,ran==false,因此不执行FutureTask#set()。

public class FutureTask<V> implements RunnableFuture<V> {
...
    protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }
...
}
  • outcome中保存异常t
  • 连续两步设置状态到EXCEPTIONAL
  • finishCompletion()执行一些清理

如果没有抛出异常在,则outcome记录正常结果;如果抛出了异常,则outcome记录异常。

如果认为正常结果和异常都属于“任务的输出”,则使用相同的变量outcome记录是合理的;同时,使用不同的结束状态区分outcome中记录的内容。

run()小结

FutureTask将用户实现的task封装为ftask,使用状态机和outcome管理ftask的执行过程。这些过程对用户是不可见的,直到用户调用get()方法。

顺道明白了Callable实例是如何执行的,为什么实现Callable#call()方法时可以将受检异常抛到外层(而Runable#run()方法则必须在方法内处理,不能抛出)。

get()

public class FutureTask<V> implements RunnableFuture<V> {
...
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
...
}
  • 5行利用定义状态的实际值判断ftask是否已完成,如果未完成(NEW、COMPLETING),则wait阻塞直到完成,该过程可抛出InterruptedException退出。
  • 待ftask完成后,调用report()报告结束状态。

5行的写法不可读,摒弃。

report()

public class FutureTask<V> implements RunnableFuture<V> {
...
    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);
    }
...
}
  • 如果结束状态为NORMAL,则outcome保存了正常结果,泛型强转,返回。
  • 7行利用定义状态的实际值判断ftask是否是被取消导致结束的(CANCELLED、INTERRUPTING、INTERRUPTED),如果是,则将抛出CancellationException。
  • 如果不是被取消的,就是执行过程中task自己抛出了异常,则outcome保存了该异常t,包装返回ExecutionException。

将异常t作为ExecutionException的cause包装起来,异常阅读方法参考你真的会阅读Java的异常信息吗?

CancellationException和ExecutionException

  • CancellationException是非受检异常,原则上可以不处理,但仍然建议处理。
  • ExecutionException是受检异常,在外层必须处理。

源码小结

  • 实现Callable#.call()时可以将受检异常抛到外层。
  • 不管实现Callable#.call()时是否抛出了受检异常,都要在FutureTask#get()时捕获ExecutionException;建议捕获CancellationException。
  • FutureTask#get()中调用了阻塞方法,因此还需要捕获InterruptedException。
  • CancellationException异常中不会给出取消原因,包括是否因为被中断。
  • 工程上建议使用超时版的FutureTask#get(),超时会抛出TimeoutException,需要处理。

反观Future#get()的API声明:

public interface Future<V> {
...
    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * @throws ExecutionException if the computation threw an
     * exception
     * @throws InterruptedException if the current thread was interrupted
     * while waiting
     */
    V get() throws InterruptedException, ExecutionException;
...
}

right。

一种正确姿势

给出一种比较全面的正确姿势,仅供参考。

int timeoutSec = 30;
try {
  MyResult result = ftask.get(timeoutSec, TimeUnit.SECONDS);
} catch (ExecutionException e) {
  Throwable t = e.getCause();
  // handle some checked exceptions
  if (t instantanceof IOExcaption) {
    xxx;
  } else if (...) {
    xxx;
  } else { // handle remained checked exceptions and unchecked exceptions
    throw new RuntimeException("xxx", t);
  }
} catch (CancellationException e) {
  xxx;
  throw new UnknownException(String.format("Task %s canceled unexpected", taskId));
} catch (TimeoutException e) {
  xxx;
  LOGGER.error(String.format("Timeout for %ds, trying to cancel task: %s", timeoutSec, taskId));
  ftask.cancel();
  LOGGER.debug(String.format("Succeed to cancel task: %s" % taskId));
} catch (InterruptedException e) {
  xxx;
}
  • 根据实际需求删减。
  • 猴子喜欢在一些语义模糊的地方加assert或抛出UnknownException代替注释。
  • 对InterruptedException的处理暂时不讨论(少有的用于控制流程的异常,猴子理解的有点模糊),读者可参考处理 InterruptedException

换风格不错,写起来快多了。


本文链接:源码|使用FutureTask的正确姿势
作者:猴子007
出处:https://monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容