赞
踩
本篇是1000期面试系列文章的第94期,持续更新中.....
回复“面试”获取优质面试资源!
上周一位同学在面试中遇到了这么一道问题:
有三个线程T1、T2、T3,如何保证顺序执行?
常规操作,启动三个线程,让其执行。
- public class ThreadDemo {
- public static void main(String[] args) {
-
- final Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("线程1");
- }
- });
-
- final Thread t2 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("线程2");
- }
- });
-
- Thread t3 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("线程3");
- }
- });
- t1.start();
- t2.start();
- t3.start();
- }
- }
运行结果:
- 线程2
- 线程1
- 线程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()
方法来判断)
下面大致看看器源码:
- public final void join() throws InterruptedException {
- //调用了另外一个有参数的join方法
- join(0);
- }
- public final synchronized void join(long millis) throws InterruptedException {
- long base = System.currentTimeMillis();
- long now = 0;
- if (millis < 0) {
- throw new IllegalArgumentException("timeout value is negative");
- }
- //0则需要一直等到目标线程run完
- if (millis == 0) {
- // 如果被调用join方法的线程是alive状态,则调用join的方法
- while (isAlive()) {
- // == this.wait(0),注意这里释放的是
- //「被调用」join方法的线程对象的锁
- wait(0);
- }
- } else {
- // 如果目标线程未run完且阻塞时间未到,
- //那么调用线程会一直等待。
- while (isAlive()) {
- long delay = millis - now;
- if (delay <= 0) {
- break;
- }
- //每次最多等待delay毫秒时间后继续争抢对象锁,获取锁后继续从这里开始的下一行执行,
- //也可能提前被notify() /notifyAll()唤醒,造成delay未一次性消耗完,
- //会继续执行while继续wait(剩下的delay)
- wait(delay);
- // 这个变量now起的不太好,叫elapsedMillis就容易理解了
- now = System.currentTimeMillis() - base;
- }
- }
- }
下面我们使用join方法来实现线程的顺序执行。
- public class ThreadDemo {
- public static void main(String[] args) {
-
- final Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("线程1");
- }
- });
-
- final Thread t2 = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- //等待线程t1执行完成后
- //本线程t2 再执行
- t1.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程2");
- }
- });
-
- Thread t3 = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- //等待线程t2执行完成后
- //本线程t3 再执行
- t2.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程3");
- }
- });
- t3.start();
- t2.start();
- t1.start();
- }
- }
运行结果:
- 线程1
- 线程2
- 线程3
不管你运行多少次上面这段代码,结果始终不变,所以,我们就解决了多个线程按照顺序执行的问题了。
下面我们来看看另外一种方案:CountDownLatch
。
我们先来说一下CountDownLatch
,然后再来使用CountDownLatch
是怎么解决多个线程顺序执行的。
CountDownLatch
是一种同步辅助,在AQS基础之上实现的一个并发工具类,让我们多个线程执行任务时,需要等待线程执行完成后,才能执行下面的语句,之前线程操作时是使用 Thread.join
方法进行等待 。
CountDownLatch
能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。它相当于是一个计数器,这个计数器的初始值就是线程的数量,每当一个任务完成后,计数器的值就会减一,当计数器的值为 0 时,表示所有的线程都已经任务了,然后在 CountDownLatch
上等待的线程就可以恢复执行接下来的任务。
下面我们就用CountDownLatch
来实现多个线程顺序执行:
- import java.util.concurrent.CountDownLatch;
-
- /**
- * 公众号:面试专栏
- * @author 小蒋学
- * CountDownLatch 实现多个线程顺序执行
- */
- public class ThreadDemo {
- public static void main(String[] args) {
-
- CountDownLatch countDownLatch1 = new CountDownLatch(0);
-
- CountDownLatch countDownLatch2 = new CountDownLatch(1);
-
- CountDownLatch countDownLatch3 = new CountDownLatch(1);
-
-
- Thread t1 = new Thread(new Work(countDownLatch1, countDownLatch2),"线程1");
- Thread t2 = new Thread(new Work(countDownLatch2, countDownLatch3),"线程2");
- Thread t3 = new Thread(new Work(countDownLatch3, countDownLatch3),"线程3");
-
- t1.start();
- t2.start();
- t3.start();
- }
-
- static class Work implements Runnable {
- CountDownLatch cOne;
- CountDownLatch cTwo;
-
- public Work(CountDownLatch cOne, CountDownLatch cTwo) {
- super();
- this.cOne = cOne;
- this.cTwo = cTwo;
- }
-
- @Override
- public void run() {
- try {
- cOne.await();
- System.out.println("执行: " + Thread.currentThread().getName());
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- cTwo.countDown();
- }
- }
- }
- }
运行结果:
- 执行: 线程1
- 执行: 线程2
- 执行: 线程3
关于CountDownLatch
实现多个线程顺序执行就这样实现了,下面我们再用线程池来实现。
在Executors 类中有个单线程池的创建方式,下面我们就用单线程池的方式来实现多个线程顺序执行。
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- /**
- * 公众号:面试专栏
- * @author 小蒋学
- * CountDownLatch 实现多个线程顺序执行
- */
- public class ThreadDemo {
- public static void main(String[] args) {
- Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("线程1");
- }
- },"线程1");
-
- Thread t2 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("线程2");
- }
- },"线程2");
-
- Thread t3 = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("线程3");
- }
- });
-
- ExecutorService executor = Executors.newSingleThreadExecutor();
- // 将线程依次加入到线程池中
- executor.submit(t1);
- executor.submit(t2);
- executor.submit(t3);
- // 及时将线程池关闭
- executor.shutdown();
- }
- }
运行结果:
- 线程1
- 线程2
- 线程3
这样我们利用单线程池
也实现了多个线程顺序执行的问题。下面再来说一种更牛的方案。
最后一种方案是使用CompletableFuture
来实现多个线程顺序执行。
在Java 8问世前想要实现任务的回调,一般有以下两种方式:
借助Future isDone
轮询以判断任务是否执行结束,并获取结果。
借助Guava
类库ListenableFuture
、FutureCallback
。(netty也有类似的实现)
Java 8 CompletableFuture
弥补了Java在异步编程方面的弱势。
在Java中异步编程,不一定非要使用rxJava,Java本身的库中的
CompletableFuture
可以很好的应对大部分的场景。
Java8
新增的CompletableFuture
则借鉴了Netty等对Future的改造,简化了异步编程的复杂性,并且提供了函数式编程的能力 。
使用Future
获得异步执行结果时,要么调用阻塞方法get()
,要么轮询看isDone()
是否为true
,这两种方法都不是很好,因为主线程也会被迫等待。
从Java 8开始引入了CompletableFuture
,它针对Future
做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
接下来我们就使用CompletableFuture
来实现多个线程顺序执行。
- import java.util.concurrent.CompletableFuture;
-
- /**
- * 公众号:面试专栏
- * @author 小蒋学
- * CountDownLatch 实现多个线程顺序执行
- */
- public class ThreadDemo {
- public static void main(String[] args) {
- Thread t1 = new Thread(new Work(),"线程1");
- Thread t2 = new Thread(new Work(),"线程2");
- Thread t3 = new Thread(new Work(),"线程3");
-
- CompletableFuture.runAsync(()-> t1.start())
- .thenRun(()->t2.start())
- .thenRun(()->t3.start());
- }
-
- static class Work implements Runnable{
- @Override
- public void run() {
- System.out.println("执行 : " + Thread.currentThread().getName());
- }
- }
- }
运行结果:
- 执行 : 线程1
- 执行 : 线程2
- 执行 : 线程3
到此,我们就使用CompletableFuture
实现了多个线程顺序执行的问题。
关于多个线程顺序执行,不管是对于面试,还是工作,关于多线程顺序执行的解决方案都是非常有必要掌握的。也希望下次面试官再问:多线程顺序执行问题的时候,你的表情应该是这样的:
好了,今天就分享到这里,如果觉得喜欢,那就在右下角里点赞、点在看。
往期推荐
面试官:为什么 SpringBoot 的 jar 可以直接运行?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。