• 一道百度java面试题的多种解法


    下面是我在2018年10月11日二面百度的时候的一个问题:

    java程序,主进程需要等待多个子进程结束之后再执行后续的代码,有哪些方案可以实现?

    这个需求其实我们在工作中经常会用到,比如用户下单一个产品,后台会做一系列的处理,为了提高效率,每个处理都可以用一个线程来执行,所有处理完成了之后才会返回给用户下单成功,欢迎大家批评指正:

    1.join方法

    使用Thread的join()等待所有的子线程执行完毕,主线程在执行,thread.join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的join()方法,直到线程A执行完毕后,才会继续执行线程B。

    示例:

     1 import java.util.Vector;
     2 
     3 public class Test {
     4     public static void main(String[] args) throws InterruptedException {
     5         Vector<Thread> vector = new Vector<>();
     6         for(int i=0;i<5;i++) {
     7             Thread childThread= new Thread(new Runnable() {
     8 
     9                 @Override
    10                 public void run() {
    11                     // TODO Auto-generated method stub
    12                     try {
    13                         Thread.sleep(1000);
    14                     } catch (InterruptedException e) {
    15                         // TODO Auto-generated catch block
    16                         e.printStackTrace();
    17                     }
    18                     System.out.println("子线程被执行");
    19                 }
    20                 
    21             });
    22             vector.add(childThread);
    23             childThread.start();
    24         }
    25         for(Thread thread : vector) {
    26             thread.join();
    27         }
    28         System.out.println("主线程被执行");
    29     }

    执行结果:

    子线程被执行
    子线程被执行
    子线程被执行
    子线程被执行
    子线程被执行
    主线程被执行

    2.等待多线程完成的CountDownLatch

    CountDownLatch的概念

    CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

    CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务。
    CountDownLatch的用法

    CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

    CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。
    CountDownLatch的不足

    CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

     1 import java.util.Vector;
     2 import java.util.concurrent.CountDownLatch;
     3 
     4 public class Test2 {
     5     public static void main(String[] args) throws InterruptedException {
     6         final CountDownLatch latch = new CountDownLatch(5);
     7         for(int i=0;i<5;i++) {
     8             Thread childThread= new Thread(new Runnable() {
     9 
    10                 @Override
    11                 public void run() {
    12                     // TODO Auto-generated method stub
    13                     try {
    14                         Thread.sleep(1000);
    15                     } catch (InterruptedException e) {
    16                         // TODO Auto-generated catch block
    17                         e.printStackTrace();
    18                     }
    19                     System.out.println("子线程被执行");
    20                     latch.countDown();
    21                 }
    22                 
    23             });
    24             
    25             childThread.start();
    26             
    27         }
    28         latch.await();//阻塞当前线程直到latch中的值
    29         System.out.println("主线程被执行");
    30     }
    31     
    32 }

    执行结果:

    子线程被执行
    子线程被执行
    子线程被执行
    子线程被执行
    子线程被执行
    主线程被执行

    3.同步屏障CyclicBarrier

    这里必须注意,CylicBarrier是控制一组线程的同步,初始化的参数:5的含义是包括主线程在内有5个线程,所以只能有四个子线程,这与CountDownLatch是不一样的。

    countDownLatch和cyclicBarrier有什么区别呢,他们的区别:countDownLatch只能使用一次,而CyclicBarrier方法可以使用reset()方法重置,所以CyclicBarrier方法可以能处理更为复杂的业务场景。

    我曾经在网上看到一个关于countDownLatch和cyclicBarrier的形象比喻,就是在百米赛跑的比赛中若使用 countDownLatch的话冲过终点线一个人就给评委发送一个人的成绩,10个人比赛发送10次,如果用CyclicBarrier,则只在最后一个人冲过终点线的时候发送所有人的数据,仅仅发送一次,这就是区别。

     1 package interview;
     2 
     3 import java.util.concurrent.BrokenBarrierException;
     4 import java.util.concurrent.CyclicBarrier;
     5 
     6 public class Test3 {
     7     public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
     8         final CyclicBarrier barrier = new CyclicBarrier(5);
     9         for(int i=0;i<4;i++) {
    10             Thread childThread= new Thread(new Runnable() {
    11 
    12                 @Override
    13                 public void run() {
    14                     // TODO Auto-generated method stub
    15                     try {
    16                         Thread.sleep(1000);
    17                     } catch (InterruptedException e) {
    18                         // TODO Auto-generated catch block
    19                         e.printStackTrace();
    20                     }
    21                     System.out.println("子线程被执行");
    22                     try {
    23                         barrier.await();
    24                     } catch (InterruptedException e) {
    25                         // TODO Auto-generated catch block
    26                         e.printStackTrace();
    27                     } catch (BrokenBarrierException e) {
    28                         // TODO Auto-generated catch block
    29                         e.printStackTrace();
    30                     }
    31                 }
    32                 
    33             });
    34             
    35             childThread.start();
    36             
    37         }
    38         barrier.await();//阻塞当前线程直到latch中的值
    39         System.out.println("主线程被执行");
    40     }
    41 }
    View Code

    执行结果:

    子线程被执行
    子线程被执行
    子线程被执行
    子线程被执行
    主线程被执行

    4.使用yield方法(注意此种方法经过亲自试验证明并不可靠!)

     1 public class Test4 {
     2     public static void main(String[] args) throws InterruptedException {
     3         for(int i=0;i<5;i++) {
     4             Thread childThread= new Thread(new Runnable() {
     5 
     6                 @Override
     7                 public void run() {
     8                     // TODO Auto-generated method stub
     9                     try {
    10                         Thread.sleep(1000);
    11                     } catch (InterruptedException e) {
    12                         // TODO Auto-generated catch block
    13                         e.printStackTrace();
    14                     }
    15                     System.out.println("子线程被执行");
    16                     
    17                 }
    18                 
    19             });
    20             
    21             childThread.start();
    22             
    23         }
    24         while (Thread.activeCount() > 2) {  //保证前面的线程都执行完
    25             Thread.yield();
    26         }
    27         System.out.println("主线程被执行");
    28     }
    29 }

    执行结果:

    1 子线程被执行
    2 子线程被执行
    3 子线程被执行
    4 子线程被执行
    5 主线程被执行
    6 子线程被执行

    为何yield方法会出现这样的问题?

    使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

    Java线程中有一个Thread.yield( )方法,很多人翻译成线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。

    打个比方:现在有很多人在排队上厕所,好不容易轮到这个人上厕所了,突然这个人说:“我要和大家来个竞赛,看谁先抢到厕所!”,然后所有的人在同一起跑线冲向厕所,有可能是别人抢到了,也有可能他自己有抢到了。我们还知道线程有个优先级的问题,那么手里有优先权的这些人就一定能抢到厕所的位置吗? 不一定的,他们只是概率上大些,也有可能没特权的抢到了。

    yield的本质是把当前线程重新置入抢CPU时间的”队列”(队列只是说所有线程都在一个起跑线上.并非真正意义上的队列)。

    5.FutureTast可用于闭锁,类似于CountDownLatch的作用

     1 import java.util.concurrent.Callable;
     2 import java.util.concurrent.ExecutionException;
     3 import java.util.concurrent.FutureTask;
     4 
     5 public class Test5 {
     6      public static void main(String[] args) {
     7         MyThread td = new MyThread();
     8           
     9         //1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
    10         FutureTask<Integer> result1 = new FutureTask<>(td);
    11         new Thread(result1).start();
    12         FutureTask<Integer> result2 = new FutureTask<>(td);
    13         new Thread(result2).start();
    14         FutureTask<Integer> result3 = new FutureTask<>(td);
    15         new Thread(result3).start();
    16           
    17         Integer sum;
    18         try {
    19                 sum = result1.get();
    20                 sum = result2.get();
    21                 sum = result3.get();
    22                 //这里获取三个sum值只是为了同步,并没有实际意义
    23                 System.out.println(sum);
    24         } catch (InterruptedException e) {
    25                 // TODO Auto-generated catch block
    26                 e.printStackTrace();
    27         } catch (ExecutionException e) {
    28                 // TODO Auto-generated catch block
    29                 e.printStackTrace();
    30         }  //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
    31             
    32         System.out.println("主线程被执行");
    33            
    34         }
    35      
    36     }
    37      
    38     class MyThread implements Callable<Integer> {
    39      
    40         @Override
    41         public Integer call() throws Exception {
    42             int sum = 0;
    43             Thread.sleep(1000);
    44             for (int i = 0; i <= 10; i++) {
    45                 sum += i;
    46             }
    47             System.out.println("子线程被执行");
    48             return sum;
    49         }
    50 }

     6.使用callable+future

    Callable+Future最终也是以Callable+FutureTask的形式实现的。
    在这种方式中调用了: Future future = executor.submit(task);

     1 import java.util.concurrent.Callable;
     2 import java.util.concurrent.ExecutionException;
     3 import java.util.concurrent.ExecutorService;
     4 import java.util.concurrent.Executors;
     5 import java.util.concurrent.Future;
     6 
     7 public class Test6 {
     8     public static void main(String[] args) throws InterruptedException, ExecutionException { 
     9         ExecutorService executor = Executors.newCachedThreadPool(); 
    10         Task task = new Task(); 
    11         Future<Integer> future1 = executor.submit(task); 
    12         Future<Integer> future2 = executor.submit(task);
    13         //获取线程执行结果,用来同步
    14         Integer result1 = future1.get();
    15         Integer result2 = future2.get();
    16         
    17         System.out.println("主线程执行");
    18         executor.shutdown();
    19         } 
    20 }
    21 class Task implements Callable<Integer>{ 
    22         @Override public Integer call() throws Exception { 
    23             int sum = 0; 
    24             //do something; 
    25             System.out.println("子线程被执行");
    26             return sum; 
    27             }
    28 }

    执行结果:

    子线程被执行
    子线程被执行
    主线程执行

    补充:

    1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

    2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

     

    CountDownLatch类实际上是使用计数器的方式去控制的,不难想象当我们初始化CountDownLatch的时候传入了一个int变量这个时候在类的内部初始化一个int的变量,每当我们调用countDownt()方法的时候就使得这个变量的值减1,而对于await()方法则去判断这个int的变量的值是否为0,是则表示所有的操作都已经完成,否则继续等待。
    实际上如果了解AQS的话应该很容易想到可以使用AQS的共享式获取同步状态的方式来完成这个功能。而CountDownLatch实际上也就是这么做的。


     

    参考文献:

    https://blog.csdn.net/u011277123/article/details/54015755/
    https://blog.csdn.net/joenqc/article/details/76794356

    https://blog.csdn.net/weixin_38553453/article/details/72921797

    https://blog.csdn.net/LightOfMiracle/article/details/73456832

    https://www.cnblogs.com/baizhanshi/p/6425209.html

    努力做伟大工程师
  • 相关阅读:
    列表常用的方法
    python 三级联动
    tkinter--gui工具:MD5
    python PublicMethod方法如下<LoggerBase有使用>
    python config/config.ini 配置文件如下
    python browser engine封装
    python 页面基类 Page封装 →driver层的封装(最底层的封装Page)
    Eclipse配置maven环境
    appium 多个测试用例<多个 **.py文件>,只执行一次app <Python 3.8.1>
    python3版本使用weditor报错,解决方案
  • 原文地址:https://www.cnblogs.com/dudu19939/p/9784116.html
Copyright © 2020-2023  润新知