昨天一个碰到了一个题目:
如何编程,使得主线程在跑完十个线程之后,再进行后续操作。
看到这个问题,我的第一反应就是可以使用Callable来实现。因为实现线程有两种方式,实现Callable接口或者实现Runnable接口(至于继承Thread类,我觉得这个方式其实本质上还是实现了Runnable接口)。但是用Runnable实现线程,我们一般是不知道线程有没有跑完的,除非经过特殊处理(如使用CountDownLatch)(ps:使用线程池的submit()可以返回一个future,调用其isDone()方法也可以确认其是否完成,2018-05-08修改)。但是如果使用Callable实现线程,我们是可以在call()方法中返回一个值,来确认此线程跑完了。因此,我们可以在主线程中收集Callable的返回值,直到收集满了十个以后,再进行后续操作,没有收集满的话,就让主线程先休眠一会儿(这个只是初步想法,后续可以优化)。
现在,我们使用Callable来实现这个目的。
class MyCallable implements Callable<Integer>{
private static int count = 0;
private static Random random = new Random();
private final int id = count ++;
public Integer call() throws Exception {
System.out.println("线程 \t" + id +"\t开始了");
//模拟工作耗时
int time = random.nextInt(3) + 1 ;
System.out.println("线程 \t" + id +"\t耗时" + time + "秒");
Thread.sleep(1000 * time);
return id;
}
}
/**
* 2018-05-04
* @author liujie
*
*/
public class TestCallable {
public static void main(String[] args) throws Exception{
int SIZE = 10;
int finished = 0;
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//我们使用一个List来存放Callable的返回值
List<Future<Integer>> futures = new ArrayList<Future<Integer>>();
for (int i = 0; i < SIZE; i++) {
Thread.sleep(100);
futures.add(cachedThreadPool.submit(new MyCallable()));
}
while (finished < SIZE) {
for (Future<Integer> future : futures) {
Integer id = future.get();
System.out.println("主线程收集的到了 \t" + id + "\t线程的返回值");
finished ++ ;
}
}
System.out.println(SIZE + "\t个线程都跑完了,继续主线程的工作");
}
}
如代码所示,我们在主线程中每隔0.1秒开启一个线程,一共开启十个,使用一个List收集callable的返回值,知道收集十个返回值为止。
控制台打印情况如下:
线程 0 耗时1秒
线程 1 耗时1秒
线程 2 耗时3秒
线程 3 耗时1秒
线程 4 耗时1秒
线程 5 耗时3秒
线程 6 耗时3秒
线程 7 耗时2秒
线程 8 耗时1秒
线程 9 耗时3秒
主线程收集的到了 0 线程的返回值
主线程收集的到了 1 线程的返回值
主线程收集的到了 2 线程的返回值
主线程收集的到了 3 线程的返回值
主线程收集的到了 4 线程的返回值
主线程收集的到了 5 线程的返回值
主线程收集的到了 6 线程的返回值
主线程收集的到了 7 线程的返回值
主线程收集的到了 8 线程的返回值
主线程收集的到了 9 线程的返回值
10 个线程都跑完了,继续主线程的工作
程序能够正常完成题目的要求,即主线程会在十个线程都完成之后再进行后续工作
但是,上面的代码其实忽略了一个重要的特性,那就是future.get()其实是阻塞的,也就是说当我们尝试用get获取返回值的时候,如果当时线程还没有跑完,这时候其实主线程就会一直等在这里,直到该线程跑完并返回一个值。
因此,以上代码可以优化成如下所示:
public class TestCallable {
public static void main(String[] args) throws Exception{
int SIZE = 10;
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//我们使用一个List来存放Callable的返回值
List<Future<Integer>> futures = new ArrayList<Future<Integer>>();
for (int i = 0; i < SIZE; i++) {
Thread.sleep(100);
futures.add(cachedThreadPool.submit(new MyCallable()));
}
for (Future<Integer> future : futures) {
Integer id = future.get();
System.out.println("主线程收集的到了 \t" + id + "\t线程的返回值");
}
System.out.println(SIZE + "\t个线程都跑完了,继续主线程的工作");
}
}
控制台的情况也跟上面的一样。
还有一点值得注意的是,
主线程收集的到了 0 线程的返回值
主线程收集的到了 1 线程的返回值
主线程收集的到了 2 线程的返回值
主线程收集的到了 3 线程的返回值
主线程收集的到了 4 线程的返回值
主线程收集的到了 5 线程的返回值
主线程收集的到了 6 线程的返回值
主线程收集的到了 7 线程的返回值
主线程收集的到了 8 线程的返回值
主线程收集的到了 9 线程的返回值
其实线程9有可能比线程0先跑完,但是由于我们在创建Future的List的时候,是按照从0开始的,在future.get()获取返回值的时候,也是先从0开始。
其实要完成题目的要求,更好地是使用CountDownLatch来完成,后续我将会补上用CountDownLatch来完成的博客。