获得多个子线程的结果,面试和工作中你会遇到的多线程问题
昨天项目组里的一名毕业生询问我,如何知道异步线程的返回值,这让我不觉想起来了两年前我参加招商系一个公司的面试,在技术二面时,面试官出过一道这样的编程题。
题目大概含义是:我有一个需求是为了得到一个求和结果,但是这个结果,需要两个耗时大概1s 左右计算功能的结果相加得到的,麻烦用线程帮我实现,方案越多越好,然后默默的递过来纸和笔~~
首先我们分析一下,这个题目肯定是需要多个线程执行的,我们要抓住这个题目的关键点 :主线程必须要等2个子线程执行完,拿到子线程的结果进行相加,得到最终结果。
其实,实现这个题目的方式有很多种,以我现在的观点来看,面试官应该主要考察如下三点:
1. 多线程相关的基本知识点理解是否透彻,thread没有返回值如何处理。
2. 思维是否活跃,知识面是否广,能想出多少种方案
3. 写代码是否规范
当年too young,我只写出了方案一和方案二的部分代码(由于平时敲代码自动提示的比较多,所以很多单词敲不出来)。
不知道面试官当时是怎样的心境,反正最终是拿到offer了,不过因为个人原因,去了另一家公司。
接下来,我将两个数相加,相个数相乘模仿两个耗时的计算功能,用三种方案来解决这个问题。(我面试的时候写的代码可没有下面这么详细)
方案一: 使用thread.join()实现
java中的join方法可以控制线程的执行顺序,这个方案主要是考察线程的join方法原理,以及thread的实现方式。
join() method suspends the execution of the calling thread until the object called finishes its execution.
大概的意思是:如果在主线程mian()中调用子线程的join()方法,就会阻塞主线程,直到子线程执行完,在唤起主线程继续执行。
至于为什么会阻塞主线程,有兴趣的同学可以继续找资料学习,这里就不多扩展了。
1 package day01; 2 3 public class JoinTest { 4 public static void main(String[] args) { 5 CalculateThread addTread = new CalculateThread(2, 3, "add"); 6 CalculateThread mutlTread = new CalculateThread(2, 3, "mutl"); 7 addTread.start(); // 子线程处理两个数相加 8 mutlTread.start(); // 子线程处理两个数相乘 9 try { 10 addTread.join(); // 暂停主线程,让addTread先执行完 11 mutlTread.join(); // 暂停主线程,让mutlTread子线程先执行完 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 16 int threadResult = addTread.getResult() + mutlTread.getResult(); 17 System.out.println("主线程获得两个子线程结果的和:" + threadResult); 18 } 19 20 } 21 22 class CalculateThread extends Thread { 23 private int param1; 24 private int param2; 25 private String type; 26 private int result; //用来保存线程执行结果 27 28 public CalculateThread(int param1, int param2, String type) { 29 this.param1 = param1; 30 this.param2 = param2; 31 this.type = type; 32 } 33 34 // 为了获取每个子线程计算结果 35 public int getResult() { 36 return result; 37 } 38 39 // 两个数相加 40 public void add() { 41 System.out.println(this.getName() + ":子线程处理两个参数相加开始"); 42 try { 43 Thread.sleep(2000L); // 模拟加法子线程需要执行2s 44 45 result = param1 + param2; 46 47 } catch (InterruptedException e) { 48 e.printStackTrace(); 49 } 50 System.out.println(this.getName() + ":子线程处理两个参数相加结束"); 51 } 52 53 // 两个数相乘 54 public void mult() { 55 System.out.println(this.getName() + ":子线程处理两个参数相乘开始"); 56 try { 57 Thread.sleep(1000L); // 模拟乘法子线程需要执行1s 58 59 result = param1 * param2; 60 61 } catch (InterruptedException e) { 62 e.printStackTrace(); 63 } 64 System.out.println(this.getName() + ":子线程处理两个参数相乘结束"); 65 } 66 67 @Override 68 public void run() { 69 // TODO Auto-generated method stub 70 switch (type) { 71 case "add": // switch的string类型,jdk 1.7才开始支持 72 add(); 73 break; 74 case "mutl": 75 mult(); 76 break; 77 default: 78 break; 79 } 80 } 81 82
方案一注意:
1. join()一定要在start()方法之后调用。所以如果多个子线程执行,要先循环执行完子线程的start()方法,再循环执行join()方法,这样才能变成并行执行。
如果执行一个子线程的start()方法后,就直接执行这个子线程的join()方法,由于主线程阻塞主了,所以需要等这个线程执行完,后面的线程才能执行,就变成串行的了。
2. 每个子线程计算的返回值,我们目前是用子线程里的变量保存实现的,我们也可以用主线程的引用类型当作共享变量实现(这个要考虑并发下,线程安全问题)。
方案二:使用Future和Callable实现
Future是可以保存返回值的,这也是很多人知道的方案,Future封装了多个方法,可以很好的获取线程执行状态,以及异常处理,这里我们就不扩展了,有兴趣的同学可以自己再去学习。
1 package day01; 2 3 import java.util.concurrent.Callable; 4 import java.util.concurrent.ExecutionException; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.Future; 8 9 public class FutureTest { 10 public static void main(String[] args) { 11 try { 12 CalculateCallable addCalable = new CalculateCallable(2, 3, "add"); 13 CalculateCallable mutlCalable = new CalculateCallable(2, 3, "mutl"); 14 // new 两个线程,固定线程 15 ExecutorService execute = Executors.newFixedThreadPool(2); 16 // 返回 Futrue对象后,可以使用 Futrue.get() 方法获取返回值 17 Future<Integer> addFuture = execute.submit(addCalable); 18 Future<Integer> mutlFuture = execute.submit(mutlCalable); 19 20 int futurResult = addFuture.get() + mutlFuture.get(); 21 System.out.println("主线程获得两个子线程结果的和:" + futurResult); 22 23 } catch (ExecutionException e) { 24 e.printStackTrace(); 25 } catch (InterruptedException e1) { 26 e1.printStackTrace(); 27 } 28 } 29 } 30 31 class CalculateCallable implements Callable<Integer> { 32 private int param1; 33 private int param2; 34 private String type; 35 36 public CalculateCallable(int param1, int param2, String type) { 37 this.param1 = param1; 38 this.param2 = param2; 39 this.type = type; 40 } 41 42 // 两个数相加 43 public int add() { 44 System.out.println("加法子线程处理两个参数相加开始"); 45 try { 46 Thread.sleep(2000L); // 模拟加法子线程需要执行2s 47 } catch (InterruptedException e) { 48 e.printStackTrace(); 49 } 50 System.out.println("加法子线程处理两个参数相加结束"); 51 52 return param1 + param2; 53 } 54 55 // 两个数相乘 56 public int mult() { 57 System.out.println("乘法子线程处理两个参数相乘开始"); 58 try { 59 Thread.sleep(1000L); // 模拟乘法子线程需要执行1s 60 } catch (InterruptedException e) { 61 e.printStackTrace(); 62 } 63 System.out.println("乘法子线程处理两个参数相乘结束"); 64 return param1 * param2; 65 66 } 67 68 @Override 69 public Integer call() throws Exception { 70 // TODO Auto-generated method stub 71 switch (type) { 72 case "add": // switch的string类型,jdk 1.7才开始支持 73 return add(); 74 case "mutl": 75 return mult(); 76 default: 77 return null; 78 } 79 } 80 81 }
方案二注意:
1. Future接口调用get()方法取得处理的结果值时是阻塞性的,如果调用Future对象的get()方法时,任务尚未执行完成,则调用get()方法时一直阻塞到此任务完成为止。
如果这样,则前面先执行的任务一旦耗时很多,后面的任务调用get()方法就呈阻塞状态,也就是排队进行等待。主线程并不能保证首先获得结果的是最先完成任务线程的返回值,大大影响运行效率。那么使用多线程就没什么意义了。
幸运的是JDK并发包也提供了CompletionService接口可以解决这个问题,它的take()方法哪个线程先完成就先获取谁的Futrue对象,有兴趣的可以去仔细了解下相关知识点。
方案三:使用CountDownLatch实现
这个方案,当时面试的时候没想到,但是是一个很好很强大的并发类。
CountDownLatch存在于java.util.concurrent包下。CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望负责启动框架服务的线程在已经启动所有的框架服务之后再执行。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
1 package day01; 2 3 import java.util.concurrent.CountDownLatch; 4 import java.util.concurrent.TimeUnit; 5 6 public class CountDownLatchTest { 7 public static void main(String[] args) { 8 // 初始值为2,因为我们目前就2个子线程 9 CountDownLatch countDownLatch = new CountDownLatch(2); 10 // 每个线程中传入countDownLatch 11 CountDownThread addTread = new CountDownThread(2,3,"add",countDownLatch); 12 CountDownThread mutlTread = new CountDownThread(2,3,"mutl",countDownLatch); 13 addTread.start(); 14 mutlTread.start(); 15 try{ 16 // 设置超时时间为3秒,3s如果线程没有执行完,返回false 17 boolean timeoutFlag = countDownLatch.await(3,TimeUnit.SECONDS); 18 if(timeoutFlag){ 19 int threadResult = addTread.getResult() + mutlTread.getResult(); 20 System.out.println("主线程获得两个子线程结果的和:" + threadResult); 21 }else{ 22 int threadResult = addTread.getResult() + mutlTread.getResult(); 23 System.out.println("主线程等待子线程执行超时:" + threadResult); 24 } 25 26 }catch (InterruptedException e){ 27 e.printStackTrace(); 28 } 29 } 30 } 31 32 33 class CountDownThread extends Thread{ 34 private int param1; 35 private int param2; 36 private String type; 37 private int result; 38 private CountDownLatch countDownLatch; 39 40 public CountDownThread(int param1,int param2,String type,CountDownLatch countDownLatch){ 41 this.param1 = param1; 42 this.param2 = param2; 43 this.type = type; 44 this.countDownLatch = countDownLatch; 45 } 46 47 // 为了获取每个子线程计算结果 48 public int getResult() { 49 return result; 50 } 51 52 // 两个数相加 53 public void add(){ 54 System.out.println(this.getName()+":子线程处理两个参数相加开始"); 55 try{ 56 Thread.sleep(2000L); //模拟加法子线程需要执行2s 57 58 result = param1 + param2; 59 60 System.out.println(this.getName()+":子线程处理两个参数相加结束"); 61 62 }catch(InterruptedException e){ 63 e.printStackTrace(); 64 }finally{ 65 // 计数器减1 66 countDownLatch.countDown(); 67 } 68 } 69 70 // 两个数相乘 71 public void mult(){ 72 System.out.println(this.getName()+":子线程处理两个参数相乘开始"); 73 try{ 74 Thread.sleep(1000L); //模拟乘法子线程需要执行1s 75 76 result = param1 * param2; 77 78 System.out.println(this.getName()+":子线程处理两个参数相乘结束"); 79 80 }catch(InterruptedException e){ 81 e.printStackTrace(); 82 }finally{ 83 // 计数器减1 84 countDownLatch.countDown(); 85 } 86 } 87 88 @Override 89 public void run() { 90 // TODO Auto-generated method stub 91 switch (type) { 92 case "add": //switch的string类型,jdk 1.7才开始支持 93 add(); 94 break; 95 case "mutl": 96 mult(); 97 break; 98 default: 99 break; 100 } 101 } 102 }
如果我们将超时时间改成1s ,boolean timeoutFlag = countDownLatch.await(1,TimeUnit.SECONDS); 由于计算相加时睡眠了2s,相乘时睡眠了1s
所以,相加的计算是直接超时的,timeoutFlag会返回false,最终计算结果是2 * 3 = 6。
方案三注意:
1. CountDownLatch的缺点是CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
2.countDownLatch.countDown() 最好在finally块中执行,防止子线程没有执行完,就自减1了,导致主线程没有等到所有子线程执行完,便执行了。
其实我们在工作中遇到的业务场景往往比较复杂,对并发,异常的处理都比较严格,这里只是给大家提供一个方向,以后遇到类似的功能需求,我们不会像无头苍蝇一样,至少有了解决问题的方向。