如何从线程返回信息——轮询、回调、Callable
考虑有这样一个LiftOff类:
/** * 类LiftOff.java的实现描述:显示发射之前的倒计时 * */ public class LiftOff implements Runnable { public LiftOff(){ taskCount++;// 计数自增 } private int countDown = 3; // 倒计时数字 private static int taskCount = 0; private int id = taskCount; @Override public void run() { while (countDown >= 0) { System.out.println("线程编号" + id + "--倒计时" + countDown); countDown--; Thread.yield(); } } }
以及一个发射主线程:
public class Launch { public static void main(String[] args) { LiftOff liftOff = new LiftOff(); Thread t = new Thread(liftOff); t.start(); System.out.println("发射!"); } }
我们的本意是先显示倒计时,然后显示“发射!”,运行结果却是
发射! 线程编号0--倒计时3 线程编号0--倒计时2 线程编号0--倒计时1 线程编号0--倒计时0
因为main()函数也是一个线程,程序能否得到正确的结果依赖于线程的相对执行速度,而我们无法控制这一点。想要使LiftOff线程执行完毕后再继续执行主线程,比较容易想到的办法是使用轮询:
我们添加了isOver变量,在倒计时结束时将isOver置为true,主函数中我们不断地判断isOver的状态,就可以判断LiftOff线程是否执行完毕:
执行main(),输出:
线程编号0--倒计时3 线程编号0--倒计时2 线程编号0--倒计时1 线程编号0--倒计时0 发射!
这个解决方案是可行的,它会以正确的顺序给出正确的结果,但是不停地查询不仅浪费性能,并且有可能会因主线程太忙于检查工作的完成情况,以至于没有给具体的工作线程留出时间,更好的方式是使用回调(callback),在线程完成时反过来调用其创建者,告诉其工作已结束:
public class LiftOff implements Runnable { private Launch launch; public LiftOff(Launch launch){ taskCount++;// 计数自增 this.launch = launch; } private int countDown = 3; // 倒计时数字 private static int taskCount = 0; private int id = taskCount; @Override public void run() { while (countDown >= 0) { System.out.println("线程编号" + id + "--倒计时" + countDown); countDown--; if(countDown < 0){ launch.callBack(); } Thread.yield(); } } }
主线程代码:
public class Launch { public void callBack(){ System.out.println("发射!"); } public static void main(String[] args) { Launch launch = new Launch(); LiftOff liftOff = new LiftOff(launch); Thread t = new Thread(liftOff); t.start(); } }
运行结果:
线程编号0--倒计时3 线程编号0--倒计时2 线程编号0--倒计时1 线程编号0--倒计时0 发射!
相比于轮询机制,回调机制的第一个优点是不会浪费那么多的CPU性能,但更重要的优点是回调更灵活,可以处理涉及更多线程,对象和类的更复杂的情况。
例如,如果有多个对象对线程的计算结果感兴趣,那么线程可以保存一个要回调的对象列表,这些对计算结果感兴趣的对象可以通过调用方法把自己添加到这个对象列表中完成注册。当线程处理完毕时,线程将回调这些对计算结果感兴趣的对象。我们可以定义一个新的接口,所有这些类都要实现这个新接口,这个新接口将声明回调方法。这种机制有一个更一般的名字:观察者(Observer)设计模式。
Callable
java5引入了多线程编程的一个新方法,可以更容易地处理回调。任务可以实现Callable接口而不是Runnable接口,通过Executor提交任务并且会得到一个Future,之后可以向Future请求得到任务结果:
public class LiftOff implements Callable<String> { public LiftOff(){ taskCount++;// 计数自增 } private int countDown = 3; // 倒计时数字 private static int taskCount = 0; private int id = taskCount; @Override public String call() throws Exception { while (countDown >= 0) { System.out.println("线程编号" + id + "--倒计时" + countDown); countDown--; } return "线程编号" + id + "--结束"; } }
主函数:
public class Launch { public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); Future<String> future = executor.submit(new LiftOff()); try { String s = future.get(); System.out.println(s); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } System.out.println("发射!"); } }
运行结果:
线程编号0--倒计时3 线程编号0--倒计时2 线程编号0--倒计时1 线程编号0--倒计时0 线程编号0--结束 发射!
容易使用Executor提交多个任务:
public class Launch { public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); List<Future<String>> results = new ArrayList<>(); //多线程执行三个任务 for (int i = 0; i < 3; i++) { Future<String> future = executor.submit(new LiftOff()); results.add(future); } //获得线程处理结果 for (Future<String> result : results) { try { String s = result.get(); System.out.println(s); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } //继续主线程流程 System.out.println("发射!"); } }
结果:
线程编号0--倒计时3 线程编号0--倒计时2 线程编号0--倒计时1 线程编号0--倒计时0 线程编号2--倒计时3 线程编号2--倒计时2 线程编号1--倒计时3 线程编号1--倒计时2 线程编号1--倒计时1 线程编号1--倒计时0 线程编号2--倒计时1 线程编号2--倒计时0 线程编号0--结束 线程编号1--结束 线程编号2--结束 发射!
可以看到,Future的get()方法,如果线程的结果已经准备就绪,会立即得到这个结果,如果还没有准备好,轮询线程会阻塞,直到结果准备就绪。
好处
使用Callable,我们可以创建很多不同的线程,然后按照需要的顺序得到我们想要的答案。另外如果有一个很耗时的计算问题,我们也可以把计算量分到多个线程中去处理,最后汇总每个线程的处理结果,从而节省时间。