资讯专栏INFORMATION COLUMN

FutureTask源码解析(1)——预备知识

mmy123456 / 1516人阅读

摘要:在分析它的源码之前我们需要先了解一些预备知识。因为接口没有返回值所以为了与兼容我们额外传入了一个参数使得返回的对象的方法直接执行的方法然后返回传入的参数。

前言

系列文章目录

FutureTask 是一个同步工具类,它实现了Future语义,表示了一种抽象的可生成结果的计算。在包括线程池在内的许多工具类中都会用到,弄懂它的实现将有利于我们更加深入地理解Java异步操作实现。

在分析它的源码之前, 我们需要先了解一些预备知识。本篇我们先来看看FutureTask 中所使用到的接口:RunnableCallableFutureRunnableFuture以及所使用到的工具类ExecutorsUnsafe

FutureTask所使用到的接口 Runnable接口

在前面Thread类源码解读的系列文章中我们说过, 创建线程最重要的是传递一个run()方法, 这个run方法定义了这个线程要做什么事情, 它被抽象成了Runnable接口:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

但是, 可以发现, 这个方法并没有任何返回值.
如果我们希望执行某种类型的操作并拿到它的执行结果, 该怎么办呢?

从 Runnable 到 Callable

要从某种类型的操作中拿到执行结果, 最简单的方式自然是令这个操作自己返回操作结果, 则相较于run方法返回void,我们可以令一个操作返回特定类型的对象, 这种思路的实现就是Callable接口:

@FunctionalInterface
public interface Callable {
    V call() throws Exception;
}

对比Callable接口与Runnable接口, 我们可以发现它们最大的不同点在于:

Callable有返回值

Callable可以抛出异常

关于有返回值这点,我们并不意外,因为这就是我们的需求,call方法的返回值类型采用的泛型,该类型是我们在创建Callable对象的时候指定的。

除了有返回值外,相较于Runnable接口,Callable还可以抛出异常,这点看上去好像没啥特别的,但是却有大用处——这意味着如果在任务执行过程中发生了异常,我们可以将它向上抛出给任务的调用者来妥善处理,我们甚至可以利用这个特性来中断一个任务的执行。而Runnable接口的run方法不能抛出异常,只能在方法内部catch住处理,丧失了一定的灵活性。

使用Callable接口解决了返回执行结果的问题, 但是也带来了一个新的问题:

如何获得执行结果?

有的同学可能就要说了, 这还不简单? 直接拿不就好了, 看我的:

public static void main(String[] args) {
    Callable myCallable = () -> "This is the results.";
    try {
        String result = myCallable.call();
        System.out.println("Callable 执行的结果是: " + result);
    } catch (Exception e) {
        System.out.println("There is a exception.");
    }
}

这种方法确实可以, 但是它存在几个问题:

call方法是在当前线程中直接调用的, 无法利用多线程。

call方法可能是一个特别耗时的操作, 这将导致程序停在myCallable.call()调用处, 无法继续运行, 直到call方法返回。

如果call方法始终不返回, 我们没办法中断它的运行。

因此, 理想的操作应当是, 我们将call方法提交给另外一个线程执行, 并在合适的时候, 判断任务是否完成, 然后获取线程的执行结果或者撤销任务, 这种思路的实现就是Future接口:

Future接口

Future接口被设计用来代表一个异步操作的执行结果。你可以用它来获取一个操作的执行结果、取消一个操作、判断一个操作是否已经完成或者是否被取消

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

Future接口一共定义了5个方法:

get()

该方法用来获取执行结果, 如果任务还在执行中, 就阻塞等待;

get(long timeout, TimeUnit unit)

该方法同get方法类似, 所不同的是, 它最多等待指定的时间, 如果指定时间内任务没有完成, 则会抛出TimeoutException异常;

cancel(boolean mayInterruptIfRunning)

该方法用来尝试取消一个任务的执行, 它的返回值是boolean类型, 表示取消操作是否成功.

isCancelled()

该方法用于判断任务是否被取消了。如果一个任务在正常执行完成之前被cancel掉了, 则返回true

isDone()

如果一个任务已经结束, 则返回true。注意, 这里的任务结束包含了以下三种情况:

任务正常执行完毕

任务抛出了异常

任务已经被取消

关于cancel方法,这里要补充说几点:
首先有以下三种情况之一的,cancel操作一定是失败的:

任务已经执行完成了

任务已经被取消过了

任务因为某种原因不能被取消

其它情况下,cancel操作将返回true。值得注意的是,cancel操作返回true并不代表任务真的就是被取消了,这取决于发动cancel状态时任务所处的状态:

如果发起cancel时任务还没有开始运行,则随后任务就不会被执行;

如果发起cancel时任务已经在运行了,则这时就需要看mayInterruptIfRunning参数了:

如果mayInterruptIfRunning 为true, 则当前在执行的任务会被中断

如果mayInterruptIfRunning 为false, 则可以允许正在执行的任务继续运行,直到它执行完

这个cancel方法的规范看起来有点绕,现在不太理解没关系,后面结合实例去看就容易弄明白了,我们将在下一篇分析FutureTask源码的时候详细说说FutureTask对这一方法的实现。

RunnableFuture 接口

RunnableFuture接口人如其名, 就是同时实现了Runnable接口和Future接口:

public interface RunnableFuture extends Runnable, Future {
    void run(); 
}

我们下一篇开始分析FutureTask的源码的时候就将看到,FutureTask实现了该接口,也就是相当于它同时实现了Runnable接口和Future接口。

有的同学可能会对这个接口产生疑惑,既然已经继承了Runnable,该接口自然就继承了run方法,为什么要在该接口的内部再写一个run方法?

单纯从理论上来说,这里确实是没有必要的,再多写一遍,我觉得大概就是为了看上去直观一点,便于文档或者UML图展示。

FutureTask所使用到的工具类 Executors

Executors 是一个用于创建线程池的工厂类,关于线程池的概念,我们以后再说。这个类同时也提供了一些有用的静态方法。

前面我们提到了Callable接口,它是JDK1.5才引入的,而Runnable接口在JDK1.0就有了,我们有时候需要将一个已经存在Runnable对象转换成Callable对象,Executors工具类为我们提供了这一实现:

public class Executors {
    /**
     * Returns a {@link Callable} object that, when
     * called, runs the given task and returns the given result.  This
     * can be useful when applying methods requiring a
     * {@code Callable} to an otherwise resultless action.
     * @param task the task to run
     * @param result the result to return
     * @param  the type of the result
     * @return a callable object
     * @throws NullPointerException if task null
     */
    public static  Callable callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter(task, result);
    }
    
    /**
     * A callable that runs given task and returns given result
     */
    static final class RunnableAdapter implements Callable {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }
}

可以明显看出来,这个方法采用了设计模式中的适配器模式,将一个Runnable类型对象适配成Callable类型。

因为Runnable接口没有返回值, 所以为了与Callable兼容, 我们额外传入了一个result参数, 使得返回的Callable对象的call方法直接执行Runnable的run方法, 然后返回传入的result参数。

有的同学要说了, 你把result参数传进去, 又原封不动的返回出来, 有什么意义呀?
这样做确实没什么意义, result参数的存在只是为了将一个Runnable类型适配成Callable类型.

Unsafe

Unsafe类对于并发编程来说是个很重要的类,如果你稍微看过J.U.C里的源码(例如我们前面讲AQS系列的文章里),你会发现到处充斥着这个类的方法调用。

这个类的最大的特点在于,它提供了硬件级别的CAS原子操作。

可能有的同学会觉得这并没有什么了不起,CAS的概念都被说烂了。但是,CAS可以说是实现了最轻量级的锁,当多个线程尝试使用CAS同时更新同一个变量时,只有其中的一个线程能成功地更新变量的值,而其他的线程将失败。然而,失败的线程并不会被挂起。

CAS操作包含了三个操作数: 需要读写的内存位置,进行比较的原值,拟写入的新值。

在Unsafe类中,实现CAS操作的方法是: compareAndSwapXXX

例如:

public native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);

obj是我们要操作的目标对象

offset表示了目标对象中,对应的属性的内存偏移量

expect是进行比较的原值

update是拟写入的新值。

所以该方法实现了对目标对象obj中的某个成员变量(field)进行CAS操作的功能。

那么,要怎么获得目标field的内存偏移量offset呢? Unsafe类为我们提供了一个方法:

public native long objectFieldOffset(Field field);

该方法的参数是我们要进行CAS操作的field对象,要怎么获得这个field对象呢?最直接的办法就是通过反射了:

Class k = FutureTask.class;
Field stateField = k.getDeclaredField("state");

这样一波下来,我们就能对FutureTask的state属性进行CAS操作了o( ̄▽ ̄)o

除了compareAndSwapObject,Unsafe类还提供了更为具体的对int和long类型的CAS操作:

public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
public native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);

从方法签名可以看出,这里只是把目标field的类型限定成int和long类型,而不是通用的Object.

最后,FutureTask还用到了一个方法:

public native void putOrderedInt(Object obj, long offset, int value);

可以看出,该方法只有三个参数,所以它没有比较再交换的概念,某种程度上就是一个赋值操作,即设置obj对象中offset偏移地址对应的int类型的field的值为指定值。这其实是Unsafe的另一个方法putIntVolatile的有序或者有延迟的版本,并且不保证值的改变被其他线程立即看到,只有在field被volatile修饰并且期望被意外修改的时候使用才有用。

那么putIntVolatile方法的定义是什么呢?

public native void putIntVolatile(Object obj, long offset, int value);

该方法设置obj对象中offset偏移地址对应的整型field的值为指定值,支持volatile store语义。由此可以看出,当操作的int类型field本身已经被volatile修饰时,putOrderedIntputIntVolatile是等价的。

好了,到这里,基本需要用到的预备知识我们都学习完了,障碍已经扫清,下一篇我们就可以愉快地看FutureTask的源码了(๑¯∀¯๑)

(完)

查看更多系列文章: 系列文章目录

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77316.html

相关文章

  • FutureTask源码解析(2)——深入理解FutureTask

    摘要:本文的源码基于。人如其名,包含了和两部分。而将一个任务的状态设置成终止态只有三种方法我们将在下文的源码解析中分析这三个方法。将栈中所有挂起的线程都唤醒后,下面就是执行方法这个方法是一个空方 前言 系列文章目录 有了上一篇对预备知识的了解之后,分析源码就容易多了,本篇我们就直接来看看FutureTask的源码。 本文的源码基于JDK1.8。 Future和Task 在深入分析源码之前,我...

    Harpsichord1207 评论0 收藏0
  • 系列文章目录

    摘要:为了避免一篇文章的篇幅过长,于是一些比较大的主题就都分成几篇来讲了,这篇文章是笔者所有文章的目录,将会持续更新,以给大家一个查看系列文章的入口。 前言 大家好,笔者是今年才开始写博客的,写作的初衷主要是想记录和分享自己的学习经历。因为写作的时候发现,为了弄懂一个知识,不得不先去了解另外一些知识,这样以来,为了说明一个问题,就要把一系列知识都了解一遍,写出来的文章就特别长。 为了避免一篇...

    lijy91 评论0 收藏0
  • 系列文章目录

    摘要:为了避免一篇文章的篇幅过长,于是一些比较大的主题就都分成几篇来讲了,这篇文章是笔者所有文章的目录,将会持续更新,以给大家一个查看系列文章的入口。 前言 大家好,笔者是今年才开始写博客的,写作的初衷主要是想记录和分享自己的学习经历。因为写作的时候发现,为了弄懂一个知识,不得不先去了解另外一些知识,这样以来,为了说明一个问题,就要把一系列知识都了解一遍,写出来的文章就特别长。 为了避免一篇...

    Yumenokanata 评论0 收藏0
  • 追踪解析 FutureTask 源码

    摘要:零前期准备文章异常啰嗦且绕弯。版本版本简介是中默认的实现类,常与结合进行多线程并发操作。所以方法的主体其实就是去唤醒被阻塞的线程。本文仅为个人的学习笔记,可能存在错误或者表述不清的地方,有缘补充 零 前期准备 0 FBI WARNING 文章异常啰嗦且绕弯。 1 版本 JDK 版本 : OpenJDK 11.0.1 IDE : idea 2018.3 2 ThreadLocal 简介 ...

    xcc3641 评论0 收藏0
  • jvm原理

    摘要:在之前,它是一个备受争议的关键字,因为在程序中使用它往往收集器理解和原理分析简称,是后提供的面向大内存区数到数多核系统的收集器,能够实现软停顿目标收集并且具有高吞吐量具有更可预测的停顿时间。 35 个 Java 代码性能优化总结 优化代码可以减小代码的体积,提高代码运行的效率。 从 JVM 内存模型谈线程安全 小白哥带你打通任督二脉 Java使用读写锁替代同步锁 应用情景 前一阵有个做...

    lufficc 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<