Java多线程专题之Callable、Future与FutureTask(含源码分析)

简介: Java多线程专题之Callable、Future与FutureTask(含源码分析)

前言

大家好,一直以来我都本着用最通俗的话理解核心的知识点, 我认为所有的难点都离不开 基础知识 的铺垫。目前正在出一个Java多线程专题长期系列教程,从入门到进阶, 篇幅会较多, 喜欢的话,给个关注❤️ ~


适合人群

  • 有一定的Java基础
  • 想学习或了解多线程开发
  • 想提高自己的同学

大佬可以绕过 ~


背景

之前给大家讲了一些框架的使用,这些都属于业务层面的东西,你需要熟练掌握它并在项目中会运用它即可,但这些对自身技术的积累是远远不够的,如果你想要提高自己,对于语言本身你需要花更多的时间去挖掘而不是局限于框架的使用,所以之前为什么跟大家一直强调基础的重要性,框架可以千变万化,层出不穷,但是基础它是不变的,不管是学java还是前端或者是其它语言, 这一点大家还是需要认清的。


接下来的几期会专门讲多线程这一块,篇幅会较多,耐心看完你一定会有收获~


情景回顾

上期带大家学习了什么是进阶学习了Thread以及分析了它的一些源码,本期带大家学习Callable、Future与FutureTask的用法以及源码分析, 内容较多, 我们一起来看一下吧~


Callable & Future

之前我们通过Runnable,Thread就可以创建一个线程,但是它也有一个局限,就是没有返回值,有时候我们的需求需要结合多任务处理后的数据做一些事情,所以通过上边的方法就不好解决了。


下面我们看一下Callable

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
复制代码


首先它是一个接口,且还提供了泛型的支持,call方法有返回值, 那怎么使用它呢,肯定是要实现它

public class CallableTest {
    public static class CallableDemo implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "hello";
        }
    }
    public static void main(String[] args) throws Exception {
        CallableDemo demo = new CallableDemo();
        String result = demo.call();
        System.out.println(result);
        System.out.println("main");
    }
}
复制代码


运行一下实际输出

hello
main
复制代码


发现返回的结果输出出去了,但是这里有个问题,这个main输出在hello之后,似乎好像没有开启一个线程,依然是同步执行的,是这样吗,我们看一下call内部的线程环境

public String call() throws Exception {
    System.out.println(Thread.currentThread());
    Thread.sleep(3000);
    return "hello";
}
复制代码


运行一下实际输出

Thread[main,5,main]
hello
main
复制代码


好家伙,还是main线程内部,并且线程还被阻塞了,原来new是开启不了线程的,只是单纯的实现了一下它的接口,我们姿势搞错了。其实它的源码上加了注释的,说通常会借助Excutors类使用,这个类是用来创建线程池的,这个我们后边讲,这里给大家演示一下

public static void main(String[] args) throws Exception {
    CallableDemo demo = new CallableDemo();
    // 创建线程池
    ExecutorService executor = Executors.newCachedThreadPool();
    // 提交任务
    Future<String> future = executor.submit(demo);
    System.out.println("main");
}
复制代码


实际输出:

main
Thread[pool-1-thread-1,5,main]
复制代码


发现是单独线程执行的,并且没有阻塞线程。我们发现这里也用到了Future,这个翻译过来时未来的意思,这里也就是结果发生在后边,它是一个异步情况, 那么我们如何获取到结果呢?

System.out.println(future.get());
System.out.println("main");
复制代码


实际输出:

Thread[pool-1-thread-1,5,main]
hello
main
复制代码


发现结果拿到了,但是运行的时候好像线程被阻塞了,我们可以发现get()会导致线程阻塞,举一反三,我想不阻塞的情况下拿到返回值,可以吗❓那有什么办法呢?开启单独的线程不就好了,那么在单独的线程可以拿到其它线程的值吗,我们来试一下

new Thread(() -> {
    try {
        System.out.println(future.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}).start();
System.out.println("main");
复制代码


实际运行输出:

Thread[pool-1-thread-1,5,main]
main
hello
复制代码


发现,这下就对了~


Future & FutureTask 源码解析

端起小板凳,这部分好好听,我们主要看下它的源码实现。我们上文使用到了 Future,我们看一下它的定义,发现它也是一个接口

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
}
复制代码


还有一个接口叫做RunnableFuture,FutureTask是它的一个实现类,这个类帮我实现了很多好用的方法,因为我们自己实现的话是很麻烦的

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}
复制代码


之前的例子也可以用FutureTask改写成:

public static void main(String[] args) throws Exception {
    CallableDemo demo = new CallableDemo();
    ExecutorService executor = Executors.newCachedThreadPool();
    FutureTask<String> futureTask = new FutureTask<>(demo);
    executor.submit(futureTask);
    System.out.println(futureTask.get());
}
复制代码


它继承了 Runnable, Future接口,我们之前调用的get方法就是其中之一,来一起看一下这个get是如何拿到值的,该部分源码来自FutureTask类实现

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}
复制代码


这个state线程的状态值,这里很好理解,一个是阻塞方法awaitDone,一个是抛出结果report,我们重点看一下awaitDone的实现:

private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                    q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}
复制代码


首先它是一个内部方法,timed指定是否定时等待,如果传true的话需要指定时间nanos

// 销亡时间  System.nanoTime() 正在运行的 Java 虚拟机的高分辨率时间源的当前值,以纳秒为单位
final long deadline = timed ? System.nanoTime() + nanos : 0L;
复制代码


WaitNode q = null;它是一个链表结构 volatile 被用来修饰会被不同线程访问和修改的变量, 后边还会讲到,此处先有个印象

static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}
复制代码


for (;;) {...},这是一个死循环,这里就是阻塞部分了,内部先会判断线程状态

// 判断线程状态 如果中断,直接抛出异常,并且将```q```从节点中移除
if (Thread.interrupted()) {
    removeWaiter(q);
    throw new InterruptedException();
}
复制代码


这里为什么会移除呢,想想看,如果不移除,内部积累太多,每次都要遍历它,如果是有竞争的情况下,是不是很浪费。这里主要是避免不必要的高额开销

// 线程状态 最先是 NEW
int s = state;
if (s > COMPLETING) {
    // 如果线程完成状态 移除q节点 并返回当前线程状态 最终通过report返回结果
    if (q != null)
        q.thread = null;
    return s;
}
复制代码


这里为什么移除?因为完成了,我只要结果就好了,不需要在进一步判断了

else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
复制代码


如果处于COMPLETING,会让出cpu时间

else if (q == null)
                q = new WaitNode();
复制代码


这个很好理解,节点不存在就创建一个

else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
复制代码


如果有新任务进来,会新建一个节点,然后利用CAS操作放入waiter链表的头部,这里是一个原子性操作,CAS的概念我们后边给大家讲,这里一切都是为了安全

compareAndSwap是个原子方法,原理是CAS,即将内存中的值与期望值进行比较,如果相等,就将内存中的值修改成新值并返回true。

else if (timed) {
    nanos = deadline - System.nanoTime();
    if (nanos <= 0L) {
        removeWaiter(q);
        return state;
    }
    LockSupport.parkNanos(this, nanos);
}
else
    LockSupport.park(this);
复制代码


这里判断消亡时间,如果超时了,移除节点,并返回线程状态,LockSupport使线程阻塞,有的同学可能会问,for不是已经阻塞了吗❓那为啥还调用LockSupport,这里其实是线程优化,想想你一直for循环一直判断是不是也会产生开销,加上LockSupport避免不要的操作,其实for的整个过程是实现了自旋锁的操作。


阻塞了不就没法执行了吗,park加锁方法还有一个对应的unpark相当于释放,但此处没有看到这个方法,那么它在哪个地方呢❓我们大体应该可以猜到,它应该是在执行阶段,还记得RunnableFuture接口下的run方法吗?下面我们看一下它的实现

public void run() {
        // 判断线程状态 如果不为NEW 或者 并判断值是否一样,如果不一样就直接返回
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            // 这一步是执行我们的任务
            Callable<V> c = callable;
            // 如果任务存在 并且处于NEW状态
            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;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            // 这一步其实是防止在中断时提交任务,内部是调用了一个Thread.yield()
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
复制代码


下面我们重点看一下这个set方法

protected void set(V v) {
        // 先比较是否相同
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            // outcome 是返回的结果或者异常 setException这里是设置异常结果 异常赋值给outcome
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // 设置最终状态
            finishCompletion();
        }
}
复制代码


UNSAFE类是一个很特殊的类,它的内部几乎都是native方法,它可以使得我们能够操作内存空间来获得更高的性能,但一般我们很少使用它,因为它不被gc控制,使用不当jvm可能都会挂了。我们重点关注一下 finishCompletion这个方法

private void finishCompletion() {
        for (WaitNode q; (q = waiters) != null;) {
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                // 遍历节点释放锁
                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;
            }
        }
        // 默认下它是一个空方法,可以用于执行完成的回调方法, 可以覆盖实现
        done();
        callable = null;        // to reduce footprint
    }
复制代码


我们可以看到在这个内部它是调了一个unpark方法的,可以看出之前awaitDone()方法内部的线程阻塞在这个地方被唤醒了, 再回回过头看awaitDone()方法,就明白为啥要调用park方法了,因为线程没有达到大于COMPLETING状态,它会一直for

最后一个就是report了,返回值

private V report(int s) throws ExecutionException {
        Object x = outcome;
        // 在set的时候 我们可以看到有设置为这个状态。 V就是传入的类型
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
复制代码


于是我们的get就拿到返回值了


FutureTask 状态

这里给大家补充一下FutureTask的状态值

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;
复制代码


state可能的状态转变路径如下:

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


结束语

本期到这里就结束了, 总结一下,本节主要讲了Callable、Future与FutureTask的常用方法,以及从问题触发,带大家分析了一下FutureTask的源码,这里大家要好好理解,不要去背,想要告诉大家的是学习要带着问题, 看源码一定要大胆猜测,冷静分析 ~


下期预告

之前提到过线程组的概念,下期就带大家学习线程组和线程的优先级。关注公众号加群,一起学习进步。关注我,不迷路, 下期不见不散 ~

相关文章
|
1天前
|
Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第13天】在计算机科学中,多线程是一种使得程序可以同时执行多个任务的技术。在Java语言中,多线程的实现主要依赖于java.lang.Thread类和java.lang.Runnable接口。本文将深入探讨Java中的多线程编程,包括其基本概念、实现方法以及一些常见的问题和解决方案。
|
1天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第13天】 在Java开发中,并发编程是一个复杂且重要的领域。它不仅关系到程序的线程安全性,也直接影响到系统的性能表现。本文将探讨Java并发编程的核心概念,包括线程同步机制、锁优化技术以及如何平衡线程安全和性能。通过分析具体案例,我们将提供实用的编程技巧和最佳实践,帮助开发者在确保线程安全的同时,提升应用性能。
10 1
|
2天前
|
Java 调度
Java一分钟之线程池:ExecutorService与Future
【5月更文挑战第12天】Java并发编程中,`ExecutorService`和`Future`是关键组件,简化多线程并提供异步执行能力。`ExecutorService`是线程池接口,用于提交任务到线程池,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。通过`submit()`提交任务并返回`Future`对象,可检查任务状态、获取结果或取消任务。注意处理`ExecutionException`和避免无限等待。实战示例展示了如何异步执行任务并获取结果。理解这些概念对提升并发性能至关重要。
17 5
|
2天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
3天前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
13 3
|
3天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
55 2
|
3天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
43 3
|
3天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
4 0
|
3天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
4天前
|
安全 Java
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解