likes
comments
collection
share

JUC并发编程(4):Callable接口、FutureTask

作者站长头像
站长
· 阅读数 18

Callable接口

参考blog.csdn.net/TZ845195485…

1、回顾:创建线程的2种方式

  • 继承Thread类
  • 实现Runnable接口
  • Callable接口
  • 线程池

1.1、继承Thread类

//注意:打印出来的结果会交替执行
public class ThreadDemo{
    public static void main(String[] args) {
        //4.创建Thread类的子类对象
        MyThread myThread=new MyThread();
        //5.调用start()方法开启线程
        //[会自动调用run方法这是JVM做的事情,源码看不到 ]
        myThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("我是主线程"+i);
        }
    }
}

class MyThread extends Thread{
    //2.重写run方法
    public void run(){
        //3.将要执行的代码写在run方法中
        for(int i=0;i<100;i++){
            System.out.println("我是线程"+i);
        }
    }
}

1.2、实现Runnable接口

public class RunnableDemo {
    public static void main(String[] args) {
        //4.创建Runnable的子类对象
        MyRunnale mr=new MyRunnale(); 
        //5.将子类对象当做参数传递给Thread的构造函数,并开启线程
        //MyRunnale taget=mr; 多态
        new Thread(mr).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是主线程"+i);
        }
    }
}

//1.定义一个类实现Runnable
class MyRunnale implements Runnable{
    //2.重写run方法
    @Override
    public void run() {
        //3.将要执行的代码写在run方法中
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是线程"+i);
        }
    }
}

1.3、两者区别

查看源码

  • 继承Thread:由于子类重写了Thread类的run() ,当调用start()时,直接找子类的run()方法
  • 实现Runnable:构造函数中传入了Runnable的引用,成员变量记住了它,start()调用run()方法时内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法

继承Thread

  • 好处是可以直接使用Thread类中的方法,代码简单
  • 弊端是如果已经有了父类,就不能用这种方法

实现Runnable接口

  • 好处是即使自己定义的线程类有了父类也没有关系,因为有了父类可以实现接口,而且接口可以多实现的
  • 弊端是不能直接使用Thread中的方法,需要先获取到线程对象后,才能得到Thread的方法,代码复杂

2、Callable接口(创建线程)

2.1、代码演示

public class ThreadNew {
    public static void main(String[] args) {
        
        //3.创建callable接口实现类的对象
        NumThead m=new NumThead();
        //4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(m);
        //5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
        new Thread(futureTask).start();
        //6.获取callable接口中call方法的返回值
        try {
            //get()方法返回值即为FutureTask构造器参数callable实现类重写的call方法的返回值
            Object sum = futureTask.get();
            System.out.println("总和是:"+sum);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

//1.创建一个实现Callable接口的实现类
class  NumThead implements Callable{

    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        //public Integer call() throws Exception {
        int sum=0;
        for(int i=1;i<=100;i++){
            System.out.println(i);
            sum+=i;
        }
        return sum;
    }
}

2.2、比较Runnable接口和Callable接口

Runnable 缺少的一项功能是,当线程终止时(即 run() 完成时),我们无法使线程返回结果。为了支持此功能,Java 中提供了 Callable 接口

Callable接口中的call方法和Runnable接口中的run方法的区别

  • Callable中的call()计算结果,如果无法计算结果,会抛出异常
  • Runnable中的run()使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用该对象的run方法
  • 总的来说:run()没有返回值,不会抛出异常。而call()有返回值,会抛出异常
//实现Runnable接口
class MyThread1 implements Runnable {
    @Override
    public void run() {
    }
}

//实现Callable接口
class MyThread2 implements Callable {
    @Override
    public Integer call() throws Exception {
        return 200;
    }
}

创建线程执行时候的区别

具体在主函数中,通过Thread线程创建接口只有Runnable这个可以new Thread(new MyThread1(),"AA").start();Callable不可以new Thread(new MyThread2(),"BB").start();

因为Thread的构造函数中没有Callable接口的参数设置,所以直接替换不可以,只能用下面这种线程创建方法 即找一个类,即和Runnable接口有关系,又和Callable接口有关系,查看源码可以发现Runnable接口有实现类FutureTask(中间对象),FutureTask的构造函数有Callable参数,因此跟上面代码演示的那样,通过FutureTask创建线程对象,然后在通过Thread调用start方法启动线程。

3、FutureTask

3.1、概述

继承体系

JUC并发编程(4):Callable接口、FutureTask

FutureTaskFuture接口的唯一的实现类

FutureTask同时实现了RunnableFuture接口。它既可以作为Runnable被线程执行,又可以作为Futrue得到Callable的返回值

构造器及常用方法

FutureTask(Callable<> callable) //创建一个FutureTask,一旦运行就执行给定的Callable

FutureTask(Runnable runnable,V result) //创建一个FutureTask,一旦运行就执行给定的Runnable那边了,并安排成功完成时get返回给定的结果

get()  //获取结果

isDone()  //判断是否计算结束

3.2、FutureTask实例化

FutureTask实例化可以有两种方式(此处用的泛型参数),如上通过两种构造器方式

  • 构造器传入实现了Callable接口的实现类对象
  • 构造器传入实现了Runnable接口的实现类对象和线程运行完毕后返回的结果result、
//实现Callable接口
class MyThread2 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 200;
    }
}
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());

假设jdk8新特性,由于是函数式接口,所以可以使用lambda表达式进行简化

//lambda表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
    System.out.println(Thread.currentThread().getName()+" come in callable");
    return 1024;
});

3.3、创建线程

创建线程的方式三:实现callable接口 ---JDK 5.0 新增

  • 创建一个实现Callable接口的实现类
  • 实现call方法,将此线程需要执行的操作声明在call()
  • 创建callable接口实现类的对象
  • 将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
  • FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用star,即new Thread(futureTask2,"lucy").start();
  • 获取callable接口中call方法的返回值:System.out.println(futureTask2.get());
public class ThreadNew {
    public static void main(String[] args) {
        //3.创建callable接口实现类的对象
        NumThead m=new NumThead();
        //4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象

        FutureTask futureTask = new FutureTask(m);
        //5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
        //FutureTask类继承了Runnable接口
        //new Runnable = futrueTask;
        new Thread(futureTask).start();

        //6.获取callable接口中call方法的返回值
        try {
            //get()方法返回值即为FutureTask构造器参数callable实现类重写的call方法的返回值
            Object sum = futureTask.get();
            System.out.println("总和是:"+sum);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
//1.创建一个实现Callable接口的实现类
class  NumThead implements Callable{
    // class  NumThead implements Callable<Integer>{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        //public Integer call() throws Exception {
        int sum=0;
        for(int i=1;i<=100;i++){
            System.out.println(i);
            sum+=i;
        }
        return sum;
    }
}

所谓的FutureTask是在不影响主任务的同时,开启单线程完成某个特别的任务,之后主线程续上单线程的结果即可(该单线程汇总给主线程只需要一次即可)。如果之后主线程再开启该单线程,可以直接获得结果,因为之前已经执行过一次了。

3.4、FutureTask原理解析

有了Runnable,为什么还要有Callable接口? 我们假设一共有四个程序需要执行,第三个程序时间很长,

  • Runnable接口会按照顺序去执行,会依次从上到下去执行,会等第三个程序执行完毕,才去执行第四个
  • Callable接口会把时间长的第三个程序单独开启一个线程去执行,第1、2、4 线程执行不受影响
  • 比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务,主线程就去做其他的事情,过一会儿才去获取子任务的执行结果

例子:

  • 老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单起个线程找班长帮忙。买水,水买回来了放桌上,我需要的时候再去get。
  • 4个同学,A算1+20,B算21+30,C算31*到40,D算41+50,是不是C的计算量有点大啊,FutureTask单起个线程给C计算,我先汇总ABD,最后等C计算完了再汇总给C,拿到最终结果
  • 高考:会做的先做,不会的放在后面做

3.5、注意事项

问题1

  1. FutureTask的意义就是我的主线程、子线程依次向下执行。并且开启一个线程执行完之后,我可以随时获取到这个线程的返回值进行汇总,而不用再去执行一次这个线程。
  2. get( )方法建议放在最后一行,防止线程阻塞(一旦调用了get( )方法,不管是否计算完成都会阻塞),当然你也可以使用异步通信来获取,见问题2
  3. get方法只会计算一次,后面的就不会计算了,直接拿以前缓存的值
  4. 一个FutureTask,多个线程调用call( )方法只会调用一次
  5. 如果需要调用call方法多次,则需要多个FutureTask

实例

public class CallableDemo  {
    public static void main(String[] args) throws Exception{

        CallAble c=new CallAble();
        //一个FutureTask
        FutureTask<Integer> futureTask=new FutureTask<>(c);
        
        //多个线程调用call()方法只会调用一次
        new Thread(futureTask,"线程A").start();
        new Thread(futureTask,"线程B").start();

        Integer integer = futureTask.get();//get()方法建议放在最后一行,防止线程阻塞
        System.out.println("integer = " + integer);
    }
}
class CallAble implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("欢迎你调用call方法");
        return 6;
    }
}

结果:

欢迎你调用call方法
integer = 6

问题2:

isDone()轮询(后面我们会用CompletableFuture来解决get( )阻塞的问题)

  1. 轮询的方式会消耗无畏的CPU资源,而且也不见得能及时地得到计算的结果
  2. 如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞
public class FutureTaskTest {
    public static void main(String[] args) throws Exception{
        
        FutureTask futureTask = new FutureTask(()->{
            try { 
                TimeUnit.SECONDS.sleep(3);  
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"coming......");
            return 1024;
        });
        
        new Thread(futureTask).start();
        
        //1.果futureTask.get()放到main线程前面,会导致main线程阻塞
        //Object o = futureTask.get();

        /*Object o = futureTask.get();//不见不散,只要出现了get()方法就会阻塞
        System.out.println("不见不散,只要出现了get()方法就会阻塞,获取到的值为:"+o);*/
        
        //2.过时不候
        //System.out.println(Thread.currentThread().getName()+"\t"+"线程来了.....");
        //Object o2 = futureTask.get(2L, TimeUnit.SECONDS);
        
        //3.使用轮询
        while(true){
            if(futureTask.isDone()){
                System.out.println("使用轮询来解决,值为:"+futureTask.get());
                break;
            }else{
                System.out.println("阻塞中**********");
            }
        }
    }
}