ThreadPoolExecutor的中的submit和FutureTask || 通过Executors 创建线程池的一些实例(Callable和Runnable的在其中的体现)

简介: ThreadPoolExecutor的中的submit和FutureTask || 通过Executors 创建线程池的一些实例(Callable和Runnable的在其中的体现)

前言:

ThreadPoolExecutor 的 void execute(Runnable command) 方法,利用这个方法虽然可以提交任务,但是却没有办法获取任务的执行结果(execute() 方法没有返回值)


而很多场景下,我们又都是需要获取任务的执行结果的。那 ThreadPoolExecutor 是否提供了相关功能呢?必须的,这么重要的功能当然需要提供了——那就是sumbit


a3d0347df57f4cbe976ab04a75eb111b.png

execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。

execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。


一、ThreadPoolExecutor的中的submit和FutureTask

Executors 本质上是 ThreadPoolExecutor 类的封装.


Executors类和ThreadPoolExecutor都是util.concurrent并发包下面的类, Executos下面的newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor、newCachedThreadPool底线的实现都是用的ThreadPoolExecutor实现的,所有ThreadPoolExecutor更加灵活。


Java 通过 ThreadPoolExecutor 提供的 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。下面我们先来介绍这 3 个 submit() 方法,这 3 个方法的方法签名如下。


 // 提交 Runnable 任务
Future<?>
    submit(Runnable task);
// 提交 Callable 任务
<T> Future<T>
    submit(Callable<T> task);
// 提文 Runnable 任务及结果引用
<T> Future<T>
    submit(Runnable task T result);

你会发现它们的返回值都是 Future 接口

Future 接口有 5 个方法


取消任务的方法 cancel()、判断任务是否已取消的方法 isCancelled()、判断任务是否已结束的方法 isDone()以及2 个获得任务执行结果的 get() 和 get(timeout, unit)


其中最后一个 get(timeout, unit) 支持超时机制。通过 Future 接口的这 5 个方法你会发现,我们提交的任务不但能够获取任务执行结果,还可以取消任务。不过需要注意的是:这两个 get() 方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用 get() 方法的线程会阻塞,直到任务执行完才会被唤醒。  


这 3 个 submit() 方法之间的区别在于方法参数不同,下面我们简要介绍一下。


1.提交 Runnable 任务 submit(Runnable task) :这个方法的参数是一个 Runnable 接口,Runnable 接口的 run() 方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的 Future 仅可以用来断言任务已经结束了,类似于 Thread.join()。

2.提交 Callable 任务 submit(Callable<T> task):这个方法的参数是一个 Callable 接口,它只有一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其 get() 方法来获取任务的执行结果。

3.提交 Runnable 任务及结果引用 submit(Runnable task, T result):这个方法很有意思,假设这个方法返回的 Future 对象是 f,f.get() 的返回值就是传给 submit() 方法的参数 result。这个方法该怎么用呢?下面这段示例代码展示了它的经典用法。需要你注意的是 Runnable 接口的实现类 Task 声明了一个有参构造函数 Task(Result r) ,创建 Task 对象的时候传入了 result 对象,这样就能在类 Task 的 run() 方法中对 result 进行各种操作了。result 相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。

那么既然Executor是对ThreadPoolExecutor的封装,那么通过Executor创建的线程池自然也同样有上述3个submit方法和1个FutureTask工具类。


二、通过Executors 创建线程池的一些实例(Callable和Runnable的在其中的体现)

package Thread;
/**
 * 用Callable和FutureTask创建线程
 */
import java.util.concurrent.*;
import java.util.concurrent.ExecutorService;
public class CallableFutureTask {
    public static void main(String[] args) {
        //第一种方式
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        executor.shutdown();
        //第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();*/
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        System.out.println("主线程在执行任务");
        try {
            System.out.println("task运行结果"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("所有任务执行完毕");
    }
}
class Task implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}


Runnable任务类型和Callable任务类型

Runnable接口、Callable接口创建线程

首先我们要知道可以通过以下两种创建线程


实现Runable接口,重写run方法。

使用Callable接口 和 Future接口创建线程。(线程处于并发状态, 默认异步执行)


实现Runable接口,重写run方法。

package Thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 线程创建——》实现Runnable接口,重写run方法
 */
class MyRunable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runable接口,重写run方法");
    }
}
public class thread3 {
// 使用了线程池
    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        MyRunable myRunable = new MyRunable();
        es.submit(myRunable); // 将我们的Runnable任务提交到我们线程池中
        es.shutdown();
//        FutureTask:是对Runnable和Callable的进一步封装,
//        相比直接把Runnable和Callable扔给线程池,FutureTask的功能更多
    }
// 未使用线程池(只是Thread)
    public static void main1(String[] args) {
        MyRunable myRunable = new MyRunable();
        Thread thread = new Thread(myRunable);
        // Thread thread1 = new Thread(new MyRunable());
        thread.start();
    }
}


使用Callable和Future创建线程

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大:call()方法可以有返回值,可以声明抛出异常。

public interface Callable<V> {
    V call() throws Exception;
}


Java5提供了Future接口来接收Callable接口中call()方法的返回值。



Callable接口是 Java5 新增的接口,不是Runnable接口的子接口,所以Callable对象不能直接作为Thread对象的target。


针对这个问题,引入了RunnableFuture接口,RunnableFuture接口是Runnable接口和Future接口的子接口,可以作为Thread对象的target 。同时,Java5提供了一个RunnableFuture接口的实现类:FutureTask ,FutureTask可以作为 Thread对象的target


bd077cba67364a3daf5d86c2f5a3b686.png

一个案例

package Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
/**
 * 和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
 *  同时创建对象的时候,
 * 》call()方法可以有返回值
 *
 * 》call()方法可以声明抛出异常
 * Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,
 * 这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
 */
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("这是用Callable创建线程的一个尝试!");
        return "xixi";
    }
}
public class thread1 {
// 只是用来Thread
    public static void main1(String[] args) throws ExecutionException, InterruptedException {
        // 1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
        MyCallable myThread = new MyCallable(); // myThread是一个Callable对象
        //2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
        FutureTask<String> futureTask = new FutureTask<String>(myThread); // 与Callable关联
        //3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)——实质上还是以Callable对象来创建并启动线程
        // FutureTask实现Future接口,说明可以从FutureTask中通过get取到任务的返回结果,也可以取消任务执行(通过interreput中断)
        Thread thread = new Thread(futureTask, "有返回值的线程");
         thread.start();
        // 4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
        System.out.println("子线程的返回值" + futureTask.get()); //get()方法会阻塞,直到子线程执行结束才返回
    }
    // 使用了线程池
    public static void main2(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        MyCallable myCallable = new MyCallable();
        es.submit(myCallable); // 你直接把Callable任务丢给线程池,获取不到call返回值
        es.shutdown();
        // FutureTask:是对Runnable和Callable的进一步封装,
        //相比直接把Runnable和Callable扔给线程池,FutureTask的功能更多
    }
    // 使用了线程池
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();
        MyCallable myCallable = new MyCallable();
        //2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
        FutureTask<String> futureTask = new FutureTask<String>(myCallable); // 与Callable关联
        es.submit(futureTask); // 你直接把Callable任务丢给线程池,获取不到call返回值
        System.out.println(futureTask.get()); // 通过futureTask打印返回值
        es.shutdown();
    }
}


使用Callable和Future创建线程的 总结

使用Callable和Future创建线程的步骤如下:(未使用线程池)

(1)定义一个类实现Callable接口,并重写call()方法,该call()方法将作为线程执行体,并且有返回值

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象

(3)使用FutureTask对象作为Thread对象的target创建并启动线程

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值


使用Callable和Future创建线程的步骤如下:(使用线程池)


(1)定义一个类实现Callable接口,并重写call()方法,该call()方法将作为线程执行体,并且有返回值

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值

(3)创建线程池

(4)通过sumbit()把封装了Callable对象的futureTask提交到线程池中

(5)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值


134075a5c9fc4364a7af25dc29d1223f.png

当然如果你用了线程池,你也可以直接提交把你实例化的Callable对象和Runnable对象提交线程池中(不用FutureTask封装)


c5b7d97b5aa044cc9788a9dd055a0bc5.png

但是这里,你直接把Callable任务丢给线程池,你获取不到call方法返回值

// FutureTask:是对Runnable和Callable的进一步封装,

//相比直接把Runnable和Callable扔给线程池,FutureTask的功能更多


Runable和Callable任务类型的区别:

两者都可以被ExecutorService执行


Callable的call()方法只能通过ExecutorService的 submit(Callable task) 方法来执行,并且返回一个 Future,是表示任务等待完成的 Future.

Runnable的run方法,无返回值,无法抛出经过检查的异常。Callable的call方法,有返回值V,并且可能抛出异常。

将Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,

并且会返回执行结果Future对象。

将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,

并且会返回执行结果Future对象,但是在该Future对象上调用的方法返回的是null.


线程池的一些补充

Runnable:

可以直接用execute或sumbit提交到线程池

Callable:

功能相比Runnable来说少很多,不能用来创建线程(要和Future接口一起),也不能直接扔给线程池的execute方法。但是其中的call方法有返回值


FutureTask 实现了 Runnable 和 Future 接口,由于实现了 Runnable 接口,所以可以将 FutureTask 对象作为任务提交给 ThreadPoolExecutor 去执行,也可以直接被 Thread 执行;又因为实现了 Future 接口,所以也能用来获得任务的执行结果


64c6422fb8b94d449cf27fc37d6f8984.png

三、总结

线程的创建

线程的创建有4种方法

1、通过继承Thread类,重写其中的run方法。

2、通过实现Runnable接口,重写其中的run方法。

我们的Runnable任务可以直接作为new Thread中的target。


3、通过实现Callable类,重写其中的call方法


但是此时你得到的MyCallable实例(callable任务)不能直接作为new Thread()中的target,放到括号中。你需要通过FutureTask包装一下你的MyCallable,得到futureTask因为FutureTask实现了Runnable接口,所以futureTask可以作为Thread类的Target——》new Thread(futureTask)


上面我们说的是没有用到线程池的情况下。

如果使用了线程池,线程池的sumbit可以提交Runnable任务和Callable任务。但是execute只能提交Runnable任务。


//        FutureTask:是对Runnable和Callable的进一步封装,

//        相比直接把Runnable和Callable扔给线程池,FutureTask的功能更多


Runnable和Callable

1.实现Runnable/Callable接口相比继承Thread类的优势

(1)适合多个线程进行资源共享

(2)可以避免java中单继承的限制

(3)增加程序的健壮性,代码和数据独立

(4)线程池只能放入Runable或Callable接口实现类,不能直接放入继承Thread的类


2.Callable和Runnable的区别

(1) Callable重写的是call()方法,Runnable重写的方法是run()方法

(2) call()方法执行后可以有返回值,run()方法没有返回值

(3) call()方法可以抛出异常,run()方法不可以

(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果 。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果

相关文章
|
6月前
|
监控 Java 调度
Java线程池ThreadPoolExecutor初略探索
Java线程池ThreadPoolExecutor初略探索
|
4月前
|
Java 开发者
奇迹时刻!探索 Java 多线程的奇幻之旅:Thread 类和 Runnable 接口的惊人对决
【8月更文挑战第13天】Java的多线程特性能显著提升程序性能与响应性。本文通过示例代码详细解析了两种核心实现方式:Thread类与Runnable接口。Thread类适用于简单场景,直接定义线程行为;Runnable接口则更适合复杂的项目结构,尤其在需要继承其他类时,能保持代码的清晰与模块化。理解两者差异有助于开发者在实际应用中做出合理选择,构建高效稳定的多线程程序。
62 7
|
1月前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
26 3
|
2月前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
43 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
35 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
41 1
|
27天前
|
Java
为什么一般采用实现Runnable接口创建线程?
因为使用实现Runnable接口的同时我们也能够继承其他类,并且可以拥有多个实现类,那么我们在拥有了Runable方法的同时也可以使用父类的方法;而在Java中,一个类只能继承一个父类,那么在继承了Thread类后我们就不能再继承其他类了。
24 0
|
2月前
|
Java C++
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
35 0
|
4月前
|
监控 Java
ThreadPoolExecutor 线程执行超时,释放线程
ThreadPoolExecutor 线程执行超时,释放线程
176 1