Java 线程池之线程返回值

前言

线程并发系列文章:

Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列

线程池系列文章:

Java 线程池之线程返回值
Java 线程池之必懂应用-原理篇(上)
Java 线程池之必懂应用-原理篇(下)

通常来说,开启线程能够提高程序的并发能力,而Thread 类里并没有任何方法可以获取到线程的执行结果。接下来,我们将一步步分析如何拿到线程的执行结果。
通过本篇文章,你将了解到:

1、原始方式 获取线程执行结果
2、FutureTask 获取线程执行结果
3、线程池 获取线程执行结果

1、原始方式 获取线程执行结果

public class ThreadRet {
    private int sum = 0;

    public static void main(String args[]) {
        ThreadRet threadRet = new ThreadRet();
        threadRet.startTest();
    }

    private void startTest() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 5;
                int b = 5;
                int c = a + b;
                //将结果赋予成员变量
                sum = c;
                System.out.println("c:" + c);
            }
        });
        t1.start();

        try {
            //等待线程执行完毕
            t1.join();
            //执行过这条语句后,说明线程已将sum赋值
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sum:" + sum);
    }
}

打印结果如下:


image.png

说明主线程已经拿到线程1的执行结果了。
原理也很简单:

  • 线程1在计算结果,那么其它线程必须要等待它执行结束了才能得到有效的结果。
  • 此时可以选择两种方式检测计算结果:轮询与等待-通知,当然是用等待-通知更有效率。
  • Thread.join 即是是用了等待-通知方式,Thread.join 一直等到目标线程执行完毕后才返回,否则阻塞等待。

Thread.join 原理请移步:Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解

2、FutureTask 获取线程执行结果

FutureTask 使用

虽然上述方式能够获取线程执行结果,然而却有如下不足之处:

1、每次都需要定义不同类型的成员变量来接收返回结果。
2、每次都需要Thread.join 阻塞等待。

想想有没有什么方法将上述功能封装起来呢?该到Callable出场了。

    private void startCall() {
        //定义Callable,具体的线程处理在call()里进行
        Callable<String> callable = new Callable() {
            @Override
            public Object call() throws Exception {
                String result = "hello world";
                //返回result
                return result;
            }
        };

        //定义FutureTask,持有Callable 引用
        FutureTask<String> futureTask = new FutureTask(callable);

        //开启线程
        new Thread(futureTask).start();

        try {
            //获取结果
            String result = futureTask.get();
            System.out.println("result:" + result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

最终打印如下:
[图片上传失败...(image-c13d3d-1633842218342)]
可以看出,能够正确获取到线程的执行结果了。

操作步骤分四步:

1、定义Callable,线程具体的工作在此处理,可以返回任意值。
2、定义FutureTask,持有Callable 引用,并且指定泛型的具体类型,该类型决定了线程最终的返回类型。实际上就是将Callable.call()返回值强转为具体类型。
3、最后构造Thread,并传入FutureTask,而FutureTask实现了Runnable。
4、通过FutureTask 获取线程执行结果。

FutureTask 原理

先看关键类的定义:

#Callable.java
public interface Callable<V> {
    //返回泛型
    V call() throws Exception;
}

Callable 只有一个方法,该方法返回泛型类型。

再看FutureTask:

#FutureTask.java
    public void run() {
        try {
            //传进来的Callable
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //执行Callable call 方法
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    ...
                }
                //记录结果
                if (ran)
                    set(result);
            }
        } finally {
            ...
        }
    }

    protected void set(V v) {
        if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) {
            //记录到成员变量 outcome里
            outcome = v;
            //CAS 修改状态
            U.putOrderedInt(this, STATE, NORMAL); // final state
            //通知等待线程执行结果的其它线程
            finishCompletion();
        }
    }

    private void finishCompletion() {
        //waiters 为链表头,该链表记录着所有等待该线程执行结果的其它线程
        for (WaitNode q; (q = waiters) != null;) {
            //CAS 不成功,则继续循环
            if (U.compareAndSwapObject(this, WAITERS, q, null)) {
                //CAS 修改成功,将链表头置空
                //遍历链表
                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;
            }
        }
        ...
    }

上述逻辑很清晰:

1、FutureTask 实现了Runnable,重写了run()方法,当线程执行时会执行run()方法,而run()最终调用了Callable的call()方法,返回值记录在成员变量outcome里。
2、当run()执行完毕后,说明结果已经出来了,将通知其它线程(唤醒)。

既然有唤醒过程,那么必然有等待过程,否则唤醒的逻辑无意义。
FutureTask 实现了Future接口,重写了get()等方法。

#FutureTask.java
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            //阻塞等待
            s = awaitDone(false, 0L);
        //处理返回值
        return report(s);
    }

    private int awaitDone(boolean timed, long nanos)
            throws InterruptedException {
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            //一些临界状态判断
            //封装为节点,加入到等待链表里
            //限时等待
            else if (timed) {
                ...
                if (state < COMPLETING)
                    //线程挂起指定的时间
                    LockSupport.parkNanos(this, parkNanos);
            }
            else
                //一直等待,直到有结果返回
                LockSupport.park(this);
        }
    }

    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            //将强转为泛型指定的类型
            return (V)x;
        ...
    }

由上可以看出:

1、FutureTask.get() 阻塞等待线程执行结果返回。
2、若是还没结果,先将自己加入到等待链表里,并且可以指定等待一定的时间,若是时间到了还是没有结果,就直接返回。
3、最后等到执行结果后,强转为想要的类型,在例子里强转为String。

整个流程用图表示如下:
[图片上传失败...(image-b36b2-1633842218342)]

对比原始方式和FutureTask方式异同点:
不同点
原始方式通过Object.wait/Object.notify 来实现等待通知,而FutureTask 通过Volatile + CAS+LockSupport 来实现等待通知。

相同点
线程执行结果都存储在成员变量里。

3、线程池 获取线程执行结果

小Demo:

    private void startPool() {
        //线程池
        ExecutorService service = Executors.newSingleThreadExecutor();
        //定义Callable
        Callable<String> callable = new Callable() {
            @Override
            public Object call() throws Exception {
                String result = "hello world";
                //返回result
                return result;
            }
        };
        //返回Future,实际上是FutureTask实例
        Future<String> future = service.submit(callable);
        try {
            System.out.println(future.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

线程池提供了三种方式获取线程执行结果,虽然使用方式不太一样,但内部都是依靠Callable+FutureTask来实现的。
第一种
<T> Future<T> submit(Callable<T> task);
传入的参数为Callable,Callable.run()决定返回值。

第二种
Future<?> submit(Runnable task);
传入参数为Runnable,Runnable.run() 没有返回值,因此此时Future.get()返回null。

第三种
<T> Future<T> submit(Runnable task, T result);
传入参数除了Runnable,还有result,虽然Runnable.run() 没有返回值,但是最终Future.get() 将会返回result。

总结

以上分析了三种方式获取线程结果(实际两种,最后两种可归结为一类),虽然做法不一样,但速途同归。
想要获取线程执行结果,无非两个核心:

1、能够知道线程何时结束。
2、能够将结果抛出(比如存储在成员变量里)。

下篇将重点分析线程池的使用与原理。

演示代码 若是有帮助,给github 点个赞呗~

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

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

推荐阅读更多精彩内容