有返回值的多线程:Callable与Future、FutureTask

有返回值的多线程:Callable与Future、FutureTask

Callable

Callable是一个泛型接口,只实现了一个call()方法,这个跟Runnable类似,但是call()方法返回了一个传入的泛型结果,并且该方法是会抛出异常的

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;
}

创建个实现类,传入了Integer类型,那么在使用这个实现类创建多线程时,获取到的返回结果也将是个Integer类型的数据。

public class CallableDemo implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return null;
    }
}

接下来看一下如何使用Callable来创建多线程,这里是直接创建并启动线程,通过线程池创建Callable线程点区别,可以移步

public class CallableDemo implements Callable<Integer> {
    public static void main(String[] args) {

        // 1.实现Callable类,实例化
        CallableDemo demo = new CallableDemo();

        // 2.1 使用FutureTask类对其进行包装
        FutureTask<Integer> futureTask = new FutureTask<>(demo);

        // 2.2 创建一个线程,并传入包装后的 futureTask
        Thread thread = new Thread(futureTask);

        // 2.3 启动线程
        thread.start();

        // 3.3 调用FutureTask类的get()方法获取线程执行的返回结果
        int result = 0;
        try {
            result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        
        System.out.println("end");
        
    }

    @Override
    public Integer call() throws Exception {
        int num = 0;
        while (5 > num){
            Thread.sleep(2000);
            num++;
            System.out.println("Num is " + num);
        }
        return num;
    }
}

//执行结果
Num is 1
Num is 2
Num is 3
Num is 4
Num is 5
5
end

通过上面的代码,我们会发现两点:

  • FutureTask 类对Callable的实现类进行了包装,那这个FutureTask又是什么
  • 结合执行结果中,明显看到主线程中打印的数值5和end都是在子线程执行完毕之后,说明FutureTask的get()方法是会阻塞主线程的

Future、FutureTask

带着前面的问题,我们来看一下Future接口和其唯一实现类FutureTask

Future接口
public interface Future<V> {

    // 尝试取消线程
    boolean cancel(boolean mayInterruptIfRunning);

    // 获取线程是否已取消
    boolean isCancelled();

    // 获取线程是否已执行完成
    boolean isDone();

    // 获取线程执行结果
    V get() throws InterruptedException, ExecutionException;

    // 获取线程执行结果,在timout时间后不再阻塞
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future接口中定义了5个方法,每个方法的作用注释中说明了,get()方法的使用在上文的代码有示例

现在需要注意cancel()方法,需要传一个布尔参数:

  • 传入false时,只取消已经提交但是还未被运行的线程
  • 传入true时,取消所有已提交的线程

我们来实际使用一下这个方法

public class CallableDemo implements Callable<Integer> {


    public static void main(String[] args) {

        CallableDemo demo = new CallableDemo();
        FutureTask<Integer> futureTask = new FutureTask<>(demo);
        Thread thread = new Thread(futureTask);
        thread.start();

        int result = 0;

        // 启动另外一个线程,3s后异步中断上述子线程
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                futureTask.cancel(true);
                System.out.println("Canceled futureTask.");
            }
        });
        thread1.start();

        try {
            result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }

    @Override
    public Integer call() throws Exception {
        int num = 0;
        while (5 > num){
            Thread.sleep(1000);
            System.out.println(num);
            num++;
        }
        
        return 1;
    }
}


看一下执行结果,子线程中断,并抛出了异常,与预期的一样

在这里插入图片描述

接下来,修改call()方法为下面的代码,主要区别是去掉了Thread.sleep()的调用,通过一个大数值条件保证执行时间

@Override
public Integer call() throws Exception {
    long num = 1000000000;
    for (long i = 2; i < num; i++) {
        if(num % i == 0){
            System.out.println(0);
        }
    }

    return 1;
}

然后将触发子线程的cancel()方法的另一个子线程中的延迟时间改为500ms,于是得到了下面的运行结果

在这里插入图片描述

结果发现,在调用了cancel()方法并且主线程抛出异常后,子线程仍然运行直至结束,这是为什么呢?我们来看一下FutureTask中cancel()方法的源码

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    //【这里发出了中断指令】
                    t.interrupt();
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}

原来cancel()方法的原理就是调用正在运行的线程的interrupt()方法即发送中断指令。Thread.sleep()方法是能够对中断指令做出相应的,所以在第一次的测试符合预期,但是当第二次测试时,并没有对Thread的interrupt()方法做出中断响应,那子线程肯定就会继续执行下去。因此,我们可以得出一个结论:当一个线程支持被取消时,就应该能够对interrupt()方法做出正确的响应

那我们再次更改一下call()方法的代码

@Override
public Integer call() throws Exception {
    long num = 1000000000;
    for (long i = 2; i < num; i++) {
        if(Thread.currentThread().isInterrupted()){
            // 当线程被中断后,跳出循环
            System.out.println("Interrupted.");
            break;
        }else{
            if(num % i == 0){
                System.out.println(num % i);
            }
        }
    }

    return 1;
}

得到了下面的运行结果,结果看来符合预期,在对中断指令做出相应之后

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值