JAVA多线程基础篇--Callable与FutureTask

1.概述

Callable是java多线程中一个函数式接口,它是在java1.5的时候出现的,它能够返回线程执行结果并且能够抛出线程执行过程中的异常。Callable的出现主要是为了弥补继承Thread或实现Runnable接口的线程执行完成后,无法获得线程执行后的结果。Callable在日常的开发中,应用的场景也比较多,但是Callable的使用,也并不是百利无一害。本文将从Callable的使用、相关源码的分析以及使用中的注意事项来进行分析,帮助大家更好的理解Callable。

函数式接口:一个接口中只包含有一个方法。接口可以使用@FunctionalInterface注解修饰,也可以不使用@FunctionalInterface注解修饰。

2.实战演练

2.1 Callable与Runnable分析

首先来看一下Callable接口与Runable接口的源码:

@FunctionalInterface
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;
}
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

由上述代码可以看出,Callable接口与Runable接口都是函数式接口,区别在于以下:

1.Runnable 接口内部是一个抽象的run()方法,该方法的返回类型为void,所以执行后无法获取返回值;
2.Callable接口内部是一个call()方法,该接口是一个泛型接口(返回类型由传入类型决定)。

2.2 一个简单的Callable案例

2.2.1 测试代码

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

@Slf4j
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        FutureTask<String> futureTask = new FutureTask(myThread);
        new Thread(futureTask, "A").start();
        String result = (String) futureTask.get();
        log.info("result:{}", result);
    }
}

@Slf4j
class MyThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        log.info("test callable()");
        TimeUnit.SECONDS.sleep(5);
        return "callable";
    }
}

运行结果如下:
在这里插入图片描述

2.3 针对上述案例的分析

1.Callable和FutureTask是什么关系,为什么要和它一起使用?
解答这个问题,首先要看一下Thread的构造函数类型,具体如下图所示:
在这里插入图片描述
由上图可知,Thread的构造函数中只接受Runable对象、线程组ThreadGroup和String类型的线程名称;JAVA线程正常启动运行的方式应该只有一种:通过调用Thread类的start()方法来启动线程(不考虑线程池的启动方式)。 很多博客概念不清,将继承Thread类、实现Runable接口、实现Callable接口算作启动线程的方式,我认为是不正确的,这只是JAVA实现多线程编程的方式,而不是真正意义上的启动线程。

所以如何能够让实现了Callable接口的线程以Thread.start()方法来启动成为了一个问题?
而另一个问题是如何返回线程执行后的结果?

为了解决上述这两个问题,JAVA1.5中引入了FutureTask类,具体类图如下所示:
在这里插入图片描述
由上述类图可知,FutureTask类实现了RunnableFuture接口,该接口同时实现了Runable和Future接口,因此FutureTask可以通过Thread类的构造方法传入,再利用Thread.start()方法进行启动。另一个FutureTask内部有接收对象为Callable的构造函数,同时有可以获取线程执行结果的get()方法(具体源码会在下节分析),来获取实现Callable接口的返回结果。
上述案例中的UML图如下:
在这里插入图片描述

2.由运行结果来看,线程被阻塞了一段时间?
这是因为Future接口定义了一系列的方法,用于控制任务和获取任务的结果。下面是Future接口上的注释:
在这里插入图片描述
上面注解的大致意思是:

{@code Future}表示异步计算的结果。提供了检查计算是否完成、等待其完成以及检索计算结果的方法。只有在计算完成后,才能使用方法{@code get}检索结果,必要时将其阻塞,直到准备就绪。取消由{@code cancel}方法执行。还提供了其他方法来确定任务是正常完成还是取消。一旦计算完成,就不能取消计算。如果希望异步计算可以被取消而且不提供可用的计算结果,同时为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。

简单翻译一下就是:Future接口获取结果是阻塞式的,并且如果不想提供可用的结果,可用返回一个null作为结果。 所以,线程阻塞是因为计算线程MyThread 休眠了5s,因而阻塞了5s。

3.代码分析

3.1 Future

Future接口表示异步计算的结果,内部定义了一系列接口,包括检查计算是否完成、等待计算完成、获取计算结果等。下面详细分析一下Future的方法:

方法名称源码解释
cancel boolean cancel(boolean mayInterruptIfRunning);调用该方法将试图取消对任务的执行,如果任务已经执行完成、已取消或无法取消,则会返回失败,当该方法调用时任务还没有开始,方法调用成功而且任务将不会再执行。如果任务已经启动,则 mayInterruptIfRunning 参数确定是否执行此任务的线程应该以试图停止任务被中断。此方法返回后调用isDone 方法将返回 true 。后续调用 isCancelled 总是返回第一次调用的返回值。
isCancelled boolean isCancelled();判断任务是否被取消,如果任务在完成前被取消,将返回 true
isDoneboolean isDone();判断任务是否完成,任务已经结束,在任务完成、任务取消、任务异常的情况下都返回 true
getV get() throws InterruptedException, ExecutionException;获取任务执行结果,调用时会阻塞线程,直至获取结果
getV get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;指定时间内获取任务执行结果,如果超过该时间未获取到,则会返回null并抛出超时异常

3.2 FutureTask

Future主要是定义了关于异步计算的一系列接口,而FutureTask是Future的具体实现类,下面详细分析一下FutureTask的源码。首先来看下FutureTask内部的属性:

    //state变量用来保存任务的状态,它的取值从0~6,一共7个值,下面会单独针对这七个状态进行解释说明
    private volatile int state;
    //callable表示接收的任务对象,提交的任务会放到这个属性中
    private Callable<V> callable;
    //保存call方法执行后的返回值
    private Object outcome; // non-volatile, protected by state reads/writes
    //保存执行当前任务的线程
    private volatile Thread runner;
    //保存用来获取任务返回值的等待队列
    private volatile WaitNode waiters;

state保存的任务状态值如下:

    //初始状态,当新建一个FutureTask任务时,默认状态为NEW,也表示当前任务未执行
    private static final int NEW          = 0;
    //任务正常执行结束中,尚未正常结束,处于一种临界状态,主要包含以下两种情况
    //1.任务执行完成,还未将结果返回给属性outcome
    //2.任务执行异常,捕获异常并进行处理,还未将异常结构返回给outcome
    private static final int COMPLETING   = 1;
    //任务正常执行完成,并将任务的返回值赋给outcome属性之后,会处于NORMAL状态
    private static final int NORMAL       = 2;
    //当前任务执行中发生了异常,异常向上抛出
    private static final int EXCEPTIONAL  = 3;
    //当前任务被取消
    private static final int CANCELLED    = 4;
    //调用cancel(true),线程中断之前的状态
    private static final int INTERRUPTING = 5;
    //调用调用cancel(true),线程中断之后的状态
    private static final int INTERRUPTED  = 6;

看完属性以后,接下来看下其内部的方法,具体如下图所示:
在这里插入图片描述

3.2.1 构造方法

    //接收实现Callable1接口的任务对象,并将初始状态值设置为NEW
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
      //接收实现Runnable接口的任务对象,并将初始状态值设置为NEW
      public FutureTask(Runnable runnable, V result) {
      	//使用了装饰者模式,将传进来的Runnable对象转换成了callable对象,外部线程通过get获取值时,获取到的可能是null,也可能是传进来的结果result
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
   

3.2.2 report(int s)

//返回已完成任务的结果,或抛出任务执行过程中的异常
 private V report(int s) throws ExecutionException {
        Object x = outcome;
        //判断状态是否为NORMAL,若是,返回结果outcome
        if (s == NORMAL)
            return (V)x;
        //判断状态值是否大于 取消状态的值,若是,表明任务被取消或被其他情况终止,抛出CancellationException异常
        if (s >= CANCELLED)
            throw new CancellationException();
        //否则,抛出线程执行异常
        throw new ExecutionException((Throwable)x);
    }

3.2.3 isCancelled()

//判断任务状态是否取消,若状态值大于等于取消状态值,返回true(表明已取消),否则返回false
public boolean isCancelled() {
        return state >= CANCELLED;
    }

3.2.4 isDone()

// 判断任务是否执行完成,若完成返回true,否则返回false
public boolean isDone() {
        //默认判断状态不为NEW的均为完成状态
        return state != NEW;
    }

3.2.5 cancel(boolean mayInterruptIfRunning)

//取消任务执行
public boolean cancel(boolean mayInterruptIfRunning) {
       //条件一:state == NEW成立,表明当前任务处于运行状态或处于线程池任务队列中
       //条件二:UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)条件成立;说明设置状态为INTERRUPTING 或CANCELLED成功
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {
        //若mayInterruptIfRunning为true
            if (mayInterruptIfRunning) {
                try {
                   //执行当前FutureTask的线程,有可能现在是null,是null的情况是:当前任务在队列中,还没有线程获取到它
                    Thread t = runner;
                    //条件成立,说明当前线程runner,正在执行task
                    if (t != null)
                    //给当前执行task的线程一个中断信号,若程序响应中断,则会走中断逻辑,否则程序啥也不做
                        t.interrupt();
                } finally { // final state
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
        	//唤醒所有调用get()方法的等待线程
            finishCompletion();
        }
        return true;
    }

3.2.6 get()

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

这里的get方法主要做了两件事:

1.如果当前任务状态值小于等于COMPLETING,调用awaitDone方法进行入队或出队操作;
2.调用report方法返回任务执行结果。

3.2.7 get(long timeout, TimeUnit unit)

 public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        //判断传入参数是否为空,为空抛出异常
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        //调用awaitDone方法传入指定时间,若达到指定时间状态仍未改变,则抛出异常
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }

3.2.8 set(V v)

protected void set(V v) {
        //使用cas方式设置当前线程状态为 完成中
        //此处也是有可能失败的,外部线程直接在set执行cas之前将任务取消了(概率较小)
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        //若设置线程状态成功,设置结果给outcome
            outcome = v;
            //将结果设置给outcome后,设置任务状态为NORMAL,任务正常结束
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

3.2.9 setException(Throwable t)

protected void setException(Throwable t) {
        //调用cas设置当前任务状态为COMPLETING
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            //调用cas设置当前任务状态为EXCEPTIONAL
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

3.2.10 run()

 public void run() {
        //条件一:state != NEW,判断当前是否已经执行或被取消
        //条件二:UNSAFE.compareAndSwapObject利用cas判断当前线程是否被其它线程抢占,若抢占,则返回true
        if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
            return;
        //执行到此处,表明任务状态一定为NEW,并且当前线程获取锁成功
        try {
            //此处callable表示我们自己实现的任务
            Callable<V> c = callable;
            //c != null
            // state == NEW 此处判断是为了防止外部线程cancel任务
            if (c != null && state == NEW) {
                V result;
                // ran == true,表明程序call()方法成功执行,未抛出异常
                // ran == false,表明程序call()方法执行异常,抛出异常
                boolean ran;
                try {
                //调用call()方法执行并获取返回结果
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                //执行call()方法异常,设置result为null,调用setException方法设置state状态
                    result = null;
                    ran = false;
                    setException(ex);
                }
                //call()方法执行成功,调用set方法设置结果,释放资源
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            //判断线程状态是否中断,若中断,则抛出异常
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

3.2.11 runAndReset()

//这个方法与run()方法类似,区别在于调用完成之后,会重置所有状态值(包括结果对象callable、当前状态s等),这里不再赘述
protected boolean runAndReset() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,null,Thread.currentThread()))
            return false;
        boolean ran = false;
        int s = state;
        try {
            Callable<V> c = callable;
            if (c != null && s == NEW) {
                try {
                    c.call(); // don't set result
                    ran = true;
                } catch (Throwable ex) {
                    setException(ex);
                }
            }
        } finally {
            runner = null;
            s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
        return ran && s == NEW;
    }

3.2.12 handlePossibleCancellationInterrupt(int s)

//这个方法的主要作用就是判断状态为INTERRUPTING的话,释放CPU资源
 private void handlePossibleCancellationInterrupt(int s) {
        //若传入状态为INTERRUPTING且当前任务状态为INTERRUPTING,调用Thread.yield()释放CPU资源
        if (s == INTERRUPTING)
            while (state == INTERRUPTING)
                Thread.yield(); 
    }

3.2.13 handlePossibleCancellationInterrupt(int s)

private void finishCompletion() {
        // assert state > COMPLETING;
        //q指向等待队列头节点
        for (WaitNode q; (q = waiters) != null;) {
        //调用cas设置waiters为null是因为怕外部线程使用cancel取消当前任务的同时也会触发finishCompletion方法
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                for (;;) {
                //将当前节点thread对象赋值给t
                    Thread t = q.thread;
                // 判断t是否为空,不为空表示当前线程在等待任务结果,将thread对象设为空(GChelp GC),同时调用upark方法唤醒当前节点对应的线程
                    if (t != null) {
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    //将q的下一个节点赋给next对象
                    WaitNode next = q.next;
                    //如果next对象为空,表明队列已经达到末尾,利用break跳出循环
                    if (next == null)
                        break;
                    //如果next不为空,先将q.next指向的内存释放,再将next重新赋值给q,开始下一轮循环
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }
        done();
        //将callable 置为空,help GC
        callable = null;        // to reduce footprint
    }

3.2.14 awaitDone(boolean timed, long nanos)

  private int awaitDone(boolean timed, long nanos) throws InterruptedException {
        //计算超时时间(精确到纳秒)
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        //引用当前线程waitNode对象 封装成WaitNode 对象
        WaitNode q = null;
        //当前线程waitNode对象是否 入队
        boolean queued = false;
        for (;;) {
            //如果线程被中断,从等待队列中移除该任务,同时抛出异常
            //此条件成立,说明当前线程被其他线程以中断方式唤醒
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }
            //s > COMPLETING表明任务可能被已完成(还未设置成NORMAL状态)或者任务被取消
            int s = state;
            if (s > COMPLETING) {
            //条件成立,表明已经为当前线程创建过node了,将q.thread == null用来helpGC
                if (q != null)
                    q.thread = null;
                return s;
            }
            //若状态为COMPLETING,则说明任务已完成,正在向NORMAL状态切换,此时释放CPU执行权
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            //第一次自旋,如果q为空,表示当前线程还未创建WaitNode对象,此时为当前线程创建waitNode对象
            else if (q == null)
                q = new WaitNode();
            //判断是否在等待队列中,若不在,利用CAS将当前等待节点置于队列首部
            //第二次自旋,当前线程已经创建了WaitNode对象,但是node对象还未入队
            else if (!queued)
                //CAS方式设置waiters引用指向当前线程node,成功的话返回true,否则表明其它线程快一步执行该操作
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            //判断是否设置超时等待
            //第三次自旋,
            else if (timed) {
                //计算结束时间与当前时间差,若结果小于0,则从等待队列中移除该等待线程,同时返回状态
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                //按照传入时间阻塞当前线程,时间到达自动解锁
                LockSupport.parkNanos(this, nanos);
            }
            //永久挂起,直至线程获取到计算结果(其它线程唤醒或当前线程被中断)
            //当前get操作的线程就会被park,线程状态会变成WAITTING状态,线程会休眠
            else
                LockSupport.park(this);
        }
    }

3.2.15 removeWaiter(WaitNode node)

 private void removeWaiter(WaitNode node) {
        if (node != null) {
            node.thread = null;
            //自旋
            retry:
            for (;;) {          // restart on removeWaiter race
                for (WaitNode pred = null, q = waiters, s; q != null; q = s) {
                    s = q.next;
                    if (q.thread != null)
                        pred = q;
                    else if (pred != null) {
                        pred.next = s;
                        if (pred.thread == null) // check for race
                            continue retry;
                    }
                    else if (!UNSAFE.compareAndSwapObject(this, waitersOffset, q, s))
                        continue retry;
                }
                break;
            }
        }
    }

这个方法的作用是:移除超时或已经被中断的等待节点 这段代码的主要流程如下图所示:
在这里插入图片描述

4.小结

1.FutureTask实现了Runnable和Future接口,它是带有状态和结果的任务,所有的操作都是围绕着任务状态展开的;
2.如果任务状态不为NEW,表明任务已经完成或已经终止;
3.任务状态的切换过程大概如下四种,如下图所示:
在这里插入图片描述
4.callable的启动方式主要有两种,一种是利用FutureTask的构造方法,将callable作为参数传入;另一种调用submit方法提交到
线程池中;
5.底层还是使用了CAS完成一系列加锁和状态更改操作。

5.参考文献

1.https://www.bilibili.com/video/BV13E411N7pp?spm_id_from=333.1007.top_right_bar_window_history.content.click
2.https://juejin.cn/post/7033201440579878943
3.https://juejin.cn/post/6944572513896628238#heading-8

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值