当前位置:   article > 正文

保证多线程顺序执行,四种方案,你知道几种?

多线程如何保证线程顺序

本篇是1000期面试系列文章的第94期,持续更新中.....

回复“面试”获取优质面试资源!

故事

上周一位同学在面试中遇到了这么一道问题:

有三个线程T1、T2、T3,如何保证顺序执行?

常规操作,启动三个线程,让其执行。

  1. public class ThreadDemo {
  2.     public static void main(String[] args) {
  3.         final Thread t1 = new Thread(new Runnable() {
  4.             @Override
  5.             public void run() {
  6.                 System.out.println("线程1");
  7.             }
  8.         });
  9.         final Thread t2 = new Thread(new Runnable() {
  10.             @Override
  11.             public void run() {
  12.                 System.out.println("线程2");
  13.             }
  14.         });
  15.         Thread t3 = new Thread(new Runnable() {
  16.             @Override
  17.             public void run() {
  18.                 System.out.println("线程3");
  19.             }
  20.         });
  21.         t1.start();
  22.         t2.start();
  23.         t3.start();
  24.     }
  25. }

运行结果:

  1. 线程2
  2. 线程1
  3. 线程3

调用三个线程的start方法,很明显是按照顺序调用的,但是每次运行出来的结果,基本上都不相同,随机性特别强。

怎么办呢?下面我们使用四种方案来实现。

方案一

我们可以利用Thread中的join方法解决线程顺序问题,下面我们来简单介绍一下join方法。

官方介绍:

Waits for this thread to die.

等待这个线程结束,也就是说当前线程等待这个线程结束后再继续执行 。

join()方法是Thread中的一个public方法,它有几个重载版本:

  • join()

  • join(long millis)   //参数为毫秒

  • join(long millis,int nanoseconds)  //第一参数为毫秒,第二个参数为纳秒

join()方法实际是利用了wait()方法(wait方法是Object中的),只不过它不用等待notify()/notifyAll(),且不受其影响。

它结束的条件是:

  • 等待时间到

  • 目标线程已经run完(通过isAlive()方法来判断)

下面大致看看器源码:

  1. public final void join() throws InterruptedException {
  2.     //调用了另外一个有参数的join方法
  3.     join(0);
  4. }
  5. public final synchronized void join(long millis) throws InterruptedException {
  6.     long base = System.currentTimeMillis();
  7.     long now = 0;
  8.     if (millis < 0) {
  9.         throw new IllegalArgumentException("timeout value is negative");
  10.     }
  11.     //0则需要一直等到目标线程run完
  12.     if (millis == 0) {
  13.         // 如果被调用join方法的线程是alive状态,则调用join的方法
  14.         while (isAlive()) {
  15.             // == this.wait(0),注意这里释放的是
  16.             //「被调用」join方法的线程对象的锁
  17.             wait(0);
  18.         }
  19.     } else {
  20.          // 如果目标线程未run完且阻塞时间未到,
  21.         //那么调用线程会一直等待。
  22.         while (isAlive()) {
  23.             long delay = millis - now;
  24.             if (delay <= 0) {
  25.                 break;
  26.             }
  27.             //每次最多等待delay毫秒时间后继续争抢对象锁,获取锁后继续从这里开始的下一行执行,
  28.             //也可能提前被notify() /notifyAll()唤醒,造成delay未一次性消耗完,
  29.             //会继续执行while继续wait(剩下的delay)
  30.             wait(delay);
  31.             // 这个变量now起的不太好,叫elapsedMillis就容易理解了
  32.             now = System.currentTimeMillis() - base;
  33.         }
  34.    }
  35. }

下面我们使用join方法来实现线程的顺序执行。

  1. public class ThreadDemo {
  2.     public static void main(String[] args) {
  3.         final Thread t1 = new Thread(new Runnable() {
  4.             @Override
  5.             public void run() {
  6.                 System.out.println("线程1");
  7.             }
  8.         });
  9.         final Thread t2 = new Thread(new Runnable() {
  10.             @Override
  11.             public void run() {
  12.                 try {
  13.                     //等待线程t1执行完成后
  14.                     //本线程t2 再执行
  15.                     t1.join();
  16.                 } catch (InterruptedException e) {
  17.                     e.printStackTrace();
  18.                 }
  19.                 System.out.println("线程2");
  20.             }
  21.         });
  22.         Thread t3 = new Thread(new Runnable() {
  23.             @Override
  24.             public void run() {
  25.                 try {
  26.                     //等待线程t2执行完成后
  27.                     //本线程t3 再执行
  28.                     t2.join();
  29.                 } catch (InterruptedException e) {
  30.                     e.printStackTrace();
  31.                 }
  32.                 System.out.println("线程3");
  33.             }
  34.         });
  35.         t3.start();
  36.         t2.start();
  37.         t1.start();
  38.     }
  39. }

运行结果:

  1. 线程1
  2. 线程2
  3. 线程3

不管你运行多少次上面这段代码,结果始终不变,所以,我们就解决了多个线程按照顺序执行的问题了。

下面我们来看看另外一种方案:CountDownLatch

方案二

我们先来说一下CountDownLatch,然后再来使用CountDownLatch是怎么解决多个线程顺序执行的。

CountDownLatch是一种同步辅助,在AQS基础之上实现的一个并发工具类,让我们多个线程执行任务时,需要等待线程执行完成后,才能执行下面的语句,之前线程操作时是使用 Thread.join方法进行等待 。

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

下面我们就用CountDownLatch来实现多个线程顺序执行:

  1. import java.util.concurrent.CountDownLatch;
  2. /**
  3.  *  公众号:面试专栏
  4.  * @author 小蒋学
  5.  *  CountDownLatch 实现多个线程顺序执行
  6.  */
  7. public class ThreadDemo {
  8.     public static void main(String[] args) {
  9.         CountDownLatch countDownLatch1 = new CountDownLatch(0);
  10.         CountDownLatch countDownLatch2 = new CountDownLatch(1);
  11.         CountDownLatch countDownLatch3 = new CountDownLatch(1);
  12.         Thread t1 = new Thread(new Work(countDownLatch1, countDownLatch2),"线程1");
  13.         Thread t2 = new Thread(new Work(countDownLatch2, countDownLatch3),"线程2");
  14.         Thread t3 = new Thread(new Work(countDownLatch3, countDownLatch3),"线程3");
  15.         t1.start();
  16.         t2.start();
  17.         t3.start();
  18.     }
  19.     static class Work implements Runnable {
  20.         CountDownLatch cOne;
  21.         CountDownLatch cTwo;
  22.         public Work(CountDownLatch cOne, CountDownLatch cTwo) {
  23.             super();
  24.             this.cOne = cOne;
  25.             this.cTwo = cTwo;
  26.         }
  27.         @Override
  28.         public void run() {
  29.             try {
  30.                 cOne.await();
  31.                 System.out.println("执行: " + Thread.currentThread().getName());
  32.             } catch (InterruptedException e) {
  33.                 e.printStackTrace();
  34.             }finally {
  35.                 cTwo.countDown();
  36.             }
  37.         }
  38.     }
  39. }

运行结果:

  1. 执行: 线程1
  2. 执行: 线程2
  3. 执行: 线程3

关于CountDownLatch实现多个线程顺序执行就这样实现了,下面我们再用线程池来实现。

方案三

在Executors 类中有个单线程池的创建方式,下面我们就用单线程池的方式来实现多个线程顺序执行。

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. /**
  4.  *  公众号:面试专栏
  5.  * @author 小蒋学
  6.  *  CountDownLatch 实现多个线程顺序执行
  7.  */
  8. public class ThreadDemo {
  9.     public static void main(String[] args) {
  10.         Thread t1 = new Thread(new Runnable() {
  11.             @Override
  12.             public void run() {
  13.                 System.out.println("线程1");
  14.             }
  15.         },"线程1");
  16.         Thread t2 = new Thread(new Runnable() {
  17.             @Override
  18.             public void run() {
  19.                 System.out.println("线程2");
  20.             }
  21.         },"线程2");
  22.         Thread t3 = new Thread(new Runnable() {
  23.             @Override
  24.             public void run() {
  25.                 System.out.println("线程3");
  26.             }
  27.         });
  28.         ExecutorService executor = Executors.newSingleThreadExecutor();
  29.         // 将线程依次加入到线程池中
  30.         executor.submit(t1);
  31.         executor.submit(t2);
  32.         executor.submit(t3);
  33.         // 及时将线程池关闭
  34.         executor.shutdown();
  35.     }
  36. }

运行结果:

  1. 线程1
  2. 线程2
  3. 线程3

这样我们利用单线程池也实现了多个线程顺序执行的问题。下面再来说一种更牛的方案。

方案四

最后一种方案是使用CompletableFuture来实现多个线程顺序执行。

在Java 8问世前想要实现任务的回调,一般有以下两种方式:

  • 借助Future isDone轮询以判断任务是否执行结束,并获取结果。

  • 借助Guava类库ListenableFutureFutureCallback。(netty也有类似的实现)

Java 8 CompletableFuture弥补了Java在异步编程方面的弱势。

在Java中异步编程,不一定非要使用rxJava,Java本身的库中的CompletableFuture可以很好的应对大部分的场景。

Java8新增的CompletableFuture则借鉴了Netty等对Future的改造,简化了异步编程的复杂性,并且提供了函数式编程的能力 。

使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。

从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

接下来我们就使用CompletableFuture来实现多个线程顺序执行。

  1. import java.util.concurrent.CompletableFuture;
  2. /**
  3.  *  公众号:面试专栏
  4.  * @author 小蒋学
  5.  *  CountDownLatch 实现多个线程顺序执行
  6.  */
  7. public class ThreadDemo {
  8.     public static void main(String[] args)  {
  9.         Thread t1 = new Thread(new Work(),"线程1");
  10.         Thread t2 = new Thread(new Work(),"线程2");
  11.         Thread t3 = new Thread(new Work(),"线程3");
  12.         CompletableFuture.runAsync(()-> t1.start())
  13.                 .thenRun(()->t2.start())
  14.                 .thenRun(()->t3.start());
  15.     }
  16.     static class Work implements Runnable{
  17.         @Override
  18.         public void run() {
  19.             System.out.println("执行 : " + Thread.currentThread().getName());
  20.         }
  21.     }
  22. }

运行结果:

  1. 执行 : 线程1
  2. 执行 : 线程2
  3. 执行 : 线程3

到此,我们就使用CompletableFuture实现了多个线程顺序执行的问题。

总结

关于多个线程顺序执行,不管是对于面试,还是工作,关于多线程顺序执行的解决方案都是非常有必要掌握的。也希望下次面试官再问:多线程顺序执行问题的时候,你的表情应该是这样的:

好了,今天就分享到这里,如果觉得喜欢,那就在右下角里点、点在看

往期推荐

面试官:为什么 SpringBoot 的 jar 可以直接运行?

面试官:MySQL 批量插入,如何不插入重复数据?

面试官:java for循环,你知道几种写法?

面试官:为什么mysql不建议执行超过3表以上的多表关联查询?

面试官:Java遍历Map集合有哪几种方式?各自效率如何?

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/197812
推荐阅读
相关标签
  

闽ICP备14008679号