Java 线程池之线程返回值

作者: 小鱼人爱编程 | 来源:发表于2021-10-10 13:05 被阅读0次

    前言

    线程并发系列文章:

    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

    相关文章

      网友评论

        本文标题:Java 线程池之线程返回值

        本文链接:https://www.haomeiwen.com/subject/gnjholtx.html