Java 并发编程(八)Future和Callable,CompletionService的使用

Future和Callable介绍

线程Thread默认情况下不具有返回值的功能,如果在需要取得返回值的情况下是极为不方便的,在Java5的并发包中可以使用Future和Callable来实现线程具有返回值的功能。
Future 中文翻译为将来、未来,Callable 中文翻译为可随时支取的、请求即付的、随时可偿还的。

Callable和Runnable的区别

接口Callable与线程功能联系很紧密,和Runnable主要区别为:
1)Callable接口中的call() 方法可以有返回值,Runnable接口中run()方法没有返回值
2)Callable接口中的call()方法可以声明抛出异常,Runnable接口中的run()方法不可以声明抛出异常。

线程池中执行Callable接口中任务后,可以使用Future接口来获取返回值。

Future和Callable的使用

ExecutorService中的submit(Callable)方法可以执行Callable任务,并且使用Future接口中的get()方法获取返回值。示例如下:
创建MyCallable对象,使其实现Callable接口

package com.futurecallable;

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

public class MyCallable implements Callable<String> {
    private int age;

    public MyCallable(int age) {
        this.age = age;
    }

    @Override
    public String call() throws Exception {
        TimeUnit.SECONDS.sleep(3);
        return "the age is : " + this.age;
    }
}

创建Run类来执行Callable任务

package com.futurecallable;

import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Run {
    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable(40);
        // 创建线程池,用于执行Callable任务
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 5L, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
        Future<String> future = executor.submit(myCallable);

        System.out.println("start : " + System.currentTimeMillis());
        try {
            // 使用get()获取任务执行结果
            System.out.println(future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("end : " + System.currentTimeMillis());
    }
}

执行结果:

start : 1507965180517
the age is : 40
end : 1507965183523

说明,从控制台打印的结果看,get()具有阻塞性的。

Future和Runnable的使用

方法submit()不仅可以传入Callable对象,还可以传入Runnable对象,说明submit()方法支持有返回值和无返回值的功能。

无返回值的示例
package com.futurecallable;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Run {
    public static void main(String[] args) {
        try {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("do sth");
                }
            };
            ExecutorService service = Executors.newCachedThreadPool();
            Future future = service.submit(runnable);
            System.out.println(future.get() + "----" + future.isDone());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果

do sth
null----true

说明,submit()方法传入Runnable对象时,future获取任务返回值方法get()则为null,方法get具有阻塞性,但是isDone()则没有。

有返回值的示例

其实我们利用submit(Runnable, T)这个方法中的第二个参数T作为任务执行结果的返回值,而不使用get()来进行获得。看一下示例
创建一个Student类,用作返回的对象

package com.futurecallable;

public class Student {
    private String stuid;
    private String stuname;
    private String stuage;

    public Student() {
        super();
    }

    public String getStuid() {
        return stuid;
    }

    public void setStuid(String stuid) {
        this.stuid = stuid;
    }

    public String getStuname() {
        return stuname;
    }

    public void setStuname(String stuname) {
        this.stuname = stuname;
    }

    public String getStuage() {
        return stuage;
    }

    public void setStuage(String stuage) {
        this.stuage = stuage;
    }

    @Override
    public String toString() {
        return "Student{" +
                "stuid='" + stuid + '\'' +
                ", stuname='" + stuname + '\'' +
                ", stuage='" + stuage + '\'' +
                '}';
    }
}

创建MyRunnable类来执行任务

package com.futurecallable;

public class MyRunnable implements Runnable {
    private Student student;

    public MyRunnable(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        student.setStuid("stu_01");
        student.setStuname("Jack");
        student.setStuage("30");
    }
}

执行任务

package com.futurecallable;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MyRunnableTest {
    public static void main(String[] args) {
        try {
            Student student = new Student();
            MyRunnable myRunnable = new MyRunnable(student);

            ExecutorService service = Executors.newCachedThreadPool();
            Future<Student> future = service.submit(myRunnable, student);
            System.out.println("begin:" + System.currentTimeMillis());
            System.out.println(future.get());
            System.out.println("end:" + System.currentTimeMillis());
            System.out.println("result:" + student);
            service.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果

begin:1507966486776
Student{stuid='stu_01', stuname='Jack', stuage='30'}
end:1507966486776
result:Student{stuid='stu_01', stuname='Jack', stuage='30'}

从结果上看出,也可以使用此方法来获取任务的返回值。

Future的缺点

接口Future的实现类之一是FutureTask.java,在使用线程池时,默认情况下也是使用FutureTask.java类作为接口Future的实现来。我们知道Callable相较于Runnable是有优势的,但是Future接口中get()方法在获取结果时是阻塞的,如果任务执行时间长,则get()方法将一直等待直到任务执行完毕,如果是这样,前面的执行任务一旦耗时很长,则后面的任务调用get()方法就得等待阻塞,大大降低了运行效率,也就是主线程无法保证先执行的任务先完成任务并返回值,这就是Future的缺点,影响效率。
我们通过下个示例来说明这种情况,并且Java5中提供了解决办法,我们后续再讲解。

创建MyCallable类,用于执行任务

package com.futurecallable;

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

public class MyCallable implements Callable<String> {
    private String username;
    private long sleepTime;

    public MyCallable(String username, long sleepTime) {
        this.username = username;
        this.sleepTime = sleepTime;
    }

    @Override
    public String call() throws Exception {
        System.out.println("username:" + username);
        TimeUnit.SECONDS.sleep(sleepTime);
        return "the age is : " + this.username;
    }
}

执行任务

package com.futurecallable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class MyCallableTest {
    public static void main(String[] args) {
        try {
            MyCallable myCallable1 = new MyCallable("username1", 5);
            MyCallable myCallable2 = new MyCallable("username2", 4);
            MyCallable myCallable3 = new MyCallable("username3", 3);
            MyCallable myCallable4 = new MyCallable("username4", 2);
            MyCallable myCallable5 = new MyCallable("username5", 1);

            List<Callable> callableList = new ArrayList<>();
            callableList.add(myCallable1);
            callableList.add(myCallable2);
            callableList.add(myCallable3);
            callableList.add(myCallable4);
            callableList.add(myCallable5);

            List<Future> futures = new ArrayList<>();

            ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());

            for (int i = 0; i < 5; i++) {
                futures.add(executor.submit(callableList.get(i)));
            }
            System.out.println("first time:" + System.currentTimeMillis());
            for (int i = 0; i < 5; i++) {
                System.out.println(futures.get(i).get() + "=====" + System.currentTimeMillis());
            }
            executor.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果

username:username1
username:username5
first time:1507967802840
username:username4
username:username2
username:username3
the result is : username1=====1507967807845
the result is : username2=====1507967807846
the result is : username3=====1507967807846
the result is : username4=====1507967807846
the result is : username5=====1507967807846

从结果上看first time:1507967802840与the result is : username1=====1507967807845时间上相差5s,而其他sleep时间均小于5s,所以在第一个执行完时,其他的线程早执行完,但是由于get的阻塞性,导致后面的线程执行完但是必须等前面的返回后才能返回结果值,这就是Future的缺点。

CompletionService的使用

Future和Callable的使用中,发现FutureTask也有自身的缺点,就是get()方法的阻塞性,为了解决这个问题,Java5中提供了CompletionService接口来解决此问题。

CompletionService的逻辑实现

接口CompletionService的实现是以异步的方式一边生产新的任务,一边处理已完成任务的结果,这是将执行任务和处理任务分开进行处理。使用take取得已完成任务的结果,并按照任务完成的时间顺序处理他们的结果。

CompletionService解决Future的缺点

修改MyCallableTest类如下

package com.futurecallable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class MyCallableTest {
    public static void main(String[] args) {
        try {
            MyCallable myCallable1 = new MyCallable("username1", 5);
            MyCallable myCallable2 = new MyCallable("username2", 4);
            MyCallable myCallable3 = new MyCallable("username3", 3);
            MyCallable myCallable4 = new MyCallable("username4", 2);
            MyCallable myCallable5 = new MyCallable("username5", 1);

            List<Callable> callableList = new ArrayList<>();
            callableList.add(myCallable1);
            callableList.add(myCallable2);
            callableList.add(myCallable3);
            callableList.add(myCallable4);
            callableList.add(myCallable5);

            ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
            // 使用CompletionService类执行任务
            CompletionService completionService = new ExecutorCompletionService(executor);
            for (int i = 0; i < 5; i++) {
                completionService.submit(callableList.get(i));
            }
            System.out.println("first time:" + System.currentTimeMillis());
            for (int i = 0; i < 5; i++) {
                System.out.println("waiting print " + (i + 1) + " result");
                System.out.println(completionService.take().get());
            }
            executor.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果

username:username1
username:username2
username:username3
username:username4
username:username5
first time:1507968871720
waiting print 1 result
the result is : username5
waiting print 2 result
the result is : username4
waiting print 3 result
the result is : username3
waiting print 4 result
the result is : username2
waiting print 5 result
the result is : username1

从结果上看,已经解决了Future的阻塞性,就是使用CompletionService接口,那个任务先执行完就先打印这个任务的返回值。

CompletionService的阻塞性

当CompletionService接口中如果没有任务被执行完,则completionService.take().get()方法还是呈现阻塞性。
再修改以上代码如下

package com.futurecallable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class MyCallableTest {
    public static void main(String[] args) {
        try {
            MyCallable myCallable1 = new MyCallable("username1", 5);
            MyCallable myCallable2 = new MyCallable("username2", 4);
            MyCallable myCallable3 = new MyCallable("username3", 3);
            MyCallable myCallable4 = new MyCallable("username4", 2);
            MyCallable myCallable5 = new MyCallable("username5", 1);

            List<Callable> callableList = new ArrayList<>();
            callableList.add(myCallable1);
            callableList.add(myCallable2);
            callableList.add(myCallable3);
            callableList.add(myCallable4);
            callableList.add(myCallable5);

            ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
            // 使用CompletionService类执行任务
            CompletionService completionService = new ExecutorCompletionService(executor);
            for (int i = 0; i < 5; i++) {
                completionService.submit(callableList.get(i));
            }
            System.out.println("first time:" + System.currentTimeMillis());
            // 将循环数设置大于实际任务数,则第6个completionService.take().get()方法则会等待任务执行完,但是已经没有任务在执行,所以程序会在此阻塞。
            for (int i = 0; i < 6; i++) {
                System.out.println("waiting print " + (i + 1) + " result");
                System.out.println(completionService.take().get());
            }
            executor.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果

从图上可看到,程序并没有执行结束,而是阻塞在此,所以我们使用此接口时要确定对应的任务数。

CompletionService接口中take()方法

方法take()获取的是最先完成任务的Future对象,就是谁执行时间最短谁最先返回。
我们还是通过示例来说明

package com.futurecallable;

import java.util.concurrent.*;

public class Run {
    public static void main(String[] args) {
        try {
            ExecutorService service = Executors.newCachedThreadPool();
            CompletionService completionService = new ExecutorCompletionService(service);
            for (int i = 0; i < 10; i++) {
                completionService.submit(new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        long sleep = (int) (Math.random() * 1000);
                        System.out.println("sleep = " + sleep + " " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                        Thread.sleep(sleep);
                        return "hongguo:" + sleep + " " + Thread.currentThread().getName();
                    }
                });
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(completionService.take().get());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果

sleep = 512 pool-1-thread-1 1507969840237
sleep = 284 pool-1-thread-3 1507969840238
sleep = 311 pool-1-thread-2 1507969840238
sleep = 652 pool-1-thread-5 1507969840238
sleep = 394 pool-1-thread-4 1507969840238
sleep = 783 pool-1-thread-7 1507969840239
sleep = 231 pool-1-thread-6 1507969840239
sleep = 449 pool-1-thread-8 1507969840239
sleep = 180 pool-1-thread-9 1507969840239
sleep = 132 pool-1-thread-10 1507969840240
hongguo:132 pool-1-thread-10
hongguo:180 pool-1-thread-9
hongguo:231 pool-1-thread-6
hongguo:284 pool-1-thread-3
hongguo:311 pool-1-thread-2
hongguo:394 pool-1-thread-4
hongguo:449 pool-1-thread-8
hongguo:512 pool-1-thread-1
hongguo:652 pool-1-thread-5
hongguo:783 pool-1-thread-7

从结果上看,sleep的值越小,越先执行完并返回。

CompletionService与Runnable的组合

同样都是使用submit()方法,所以我们也可以使用CompletionService来实现Runnable接口的返回值。
我们修改MyRunnableTest类如下

package com.futurecallable;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MyRunnableTest {
    public static void main(String[] args) {
        try {
            Student student = new Student();
            MyRunnable myRunnable = new MyRunnable(student);

            Executor executor = Executors.newCachedThreadPool();
            CompletionService service = new ExecutorCompletionService(executor);
            Future<Student> future = service.submit(myRunnable, student);
            System.out.println("begin:" + System.currentTimeMillis());
            System.out.println(future.get());
            System.out.println("end:" + System.currentTimeMillis());
            System.out.println("result:" + student);
            service.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果与之前的类一样。

总结

Future和Callable解决了多线程中返回值的问题,但是其也是有缺点的就是get()是阻塞性地,为了解决这个问题,我们又引入了CompletionService接口,此接口完全避免了FutureTask的阻塞的缺点,可更加有效的处理Future的返回值,就是先执行完的任务,就先返回结果值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值