Java 线程池之线程返回值

2,095 阅读6分钟

前言

线程并发系列文章:

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.png


可以看出,能够正确获取到线程的执行结果了。

操作步骤分四步:

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.png

对比原始方式和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来实现的。
第一种
Future submit(Callable task); 传入的参数为Callable,Callable.run()决定返回值。

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

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

总结

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

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

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

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

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

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