JUC并发编程(4):Callable接口、FutureTask
Callable接口
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、概述
继承体系
FutureTask
是Future
接口的唯一的实现类
FutureTask
同时实现了Runnable
、Future
接口。它既可以作为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
- FutureTask的意义就是我的主线程、子线程依次向下执行。并且开启一个线程执行完之后,我可以随时获取到这个线程的返回值进行汇总,而不用再去执行一次这个线程。
- get( )方法建议放在最后一行,防止线程阻塞(一旦调用了get( )方法,不管是否计算完成都会阻塞),当然你也可以使用异步通信来获取,见问题2
- get方法只会计算一次,后面的就不会计算了,直接拿以前缓存的值
- 一个FutureTask,多个线程调用call( )方法只会调用一次
- 如果需要调用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( )
阻塞的问题)
- 轮询的方式会消耗无畏的CPU资源,而且也不见得能及时地得到计算的结果
- 如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞
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("阻塞中**********");
}
}
}
}
转载自:https://juejin.cn/post/7071108798131208200