顺序不乱,线程不乱跳!如何确保三个线程有序执行?

2,723 阅读7分钟

👨‍🎓作者:bug菌
✏️博客:CSDN、掘金、infoQ、51CTO等
🎉简介:CSDN | 阿里云 | 华为云 | 51CTO等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金 | InfoQ | 51CTO等社区优质创作者,全网粉丝合计15w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。

...

✍️温馨提醒:本文字数:5999字, 阅读完需:约 10 分钟

1. 前言

  哈喽,小伙伴们,你们好呀,今天我们就不整那枯燥无味的知识点了,偶尔换换口味,我们来玩点高级的;由于很多小伙伴都在给我传递一种负面情绪,今年的工作很难找,我就在想是不是八股文没准备充足啊?于是我就在总结高频笔试题,借此想把整理到的笔试题进行集合式的讨论,不仅帮助大家理解,也能帮助自己加深理解,何乐而不为呢。

  那么,具体如何实现呢?这将又会是干货满满的一期,全程无尿点不废话只抓重点教,具有非常好的学习效果,拿好小板凳准备就坐!希望学习的过程中大家认真听好好学,学习的途中有任何不清楚或疑问的地方皆可评论区留言或私信,bug菌将第一时间给予解惑,那么废话不多说,直接开整!Fighting!!

2. 环境说明

环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE

3. 需求分析

  现有T1,T2,T3三个线程,请问你如何保证T1,T2,T3三线程按顺序(T1->T2->T3)执行?比如T2 在 T1 执行完后执行,T3 在 T2 执行完后执行.

  当你看到这道题,你们的第一反应是什么?反正我首先想到的就是 thread.join()方法,因为既满足题意也是最容易实现的,hh。

  思路1:使用join。不是要保证线程顺序执行嘛,thread.Join()作用就是把指定的线程加入到当前线程中,可以把两个并行执行的线程合并为顺序执行的线程,通俗点讲就是比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会开始执行线程B,以此类推,不就满足了题目要求。

  思路2:使用CountDownLatch。CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件达到后再执行,可设置条件过滤。为什么这题要用它,是因为其内部提供了一个计数器,另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已达到,所有因调用await方法而阻塞的线程都会被唤醒;这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行。

  思路3:使用单个线程池。newSingleThreadExecutor返回以个包含单线程的Executor,将多个任务交给此Exector时,这个线程处理完一个任务后接着处理下一个任务,以此达到顺序执行三个线程的目的。

4. 代码演示

        如下我将上述提供的三种思路进行代码实例演示,希望能辅助大家理解,代码仅供参考,如有疑问,评论区记得踢我,三人行,必有我师焉。

思路1【使用join】

思路1【使用join】代码示例:

public class Test1 {

    // T1、T2、T3三个线程顺序执行
    public static void main(String[] args) {
        Thread t1 = new Thread(new runThread(null));
        Thread t2 = new Thread(new runThread(t1));
        Thread t3 = new Thread(new runThread(t2));
        t1.start();
        t2.start();
       t3.start();
    }

    static class runThread implements Runnable {
        //上一个线程
        private Thread beforeThread;

        //构造赋值
        public runThread(Thread beforeThread) {
            this.beforeThread = beforeThread;
        }

        //线程执行。
        public void run() {
            //保证第一个线程执行。
            if (beforeThread != null) {
                try {
                    //执行join(),合并为顺序执行的线程。
                    beforeThread.join();
                    System.out.println("thread start:" + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                System.out.println("thread start:" + Thread.currentThread().getName());
            }
        }
    }
}

代码解析:

  在这段代码中,分别实现了三个线程(T1、T2、T3)的顺序执行。在main函数中,先创建了t1、t2、t3三个线程,并分别传入了前置线程beforeThread:t1的前置线程为null,t2的前置线程为t1,t3的前置线程为t2。然后启动了三个线程。在run方法中,根据前置线程是否为null,使用join()方法实现线程合并,保证了T1、T2、T3的顺序执行。同时,每个线程开始执行时都会打印“thread start:”和线程名。

执行结果截图:

思路2【使用CountDownLatch】

思路2【使用CountDownLatch】代码示例:

public class Test2 {

    public static void main(String[] args) {

        CountDownLatch c1 = new CountDownLatch(0);//计数器为0
        CountDownLatch c2 = new CountDownLatch(1);//计数器为1
        CountDownLatch c3 = new CountDownLatch(1);//计数器为1

        //t1的计数器c1为0,t1执行;t2的计数器减1
        Thread t1 = new Thread(new runThread(c1, c2));

        //t2的计数器c2为0时,t2执行;t3的计数器c3减1
        Thread t2 = new Thread(new runThread(c2, c3));

        //t3的计数器c3为0时,t3执行.
        Thread t3 = new Thread(new runThread(c3, c3));

        t1.start();
        t2.start();
        t3.start();
    }

    static class runThread implements Runnable {
        private CountDownLatch c1;
        private CountDownLatch c2;

        public runThread(CountDownLatch c1, CountDownLatch c2) {
            this.c1 = c1;
            this.c2 = c2;
        }

        @Override
        public void run() {
            try {
                //计数为0才可以执行
                c1.await();
                System.out.println("thread start:" + Thread.currentThread().getName());
                //后一线程计数器减少
                c2.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

代码解析:

  在这段代码中,分别创建了三个线程(t1、t2、t3),分别对应三个CountDownLatch对象(c1、c2、c3)。三个CountDownLatch对象的初始值分别为0、1、1。t1和t2的计数器初始值是1,t3的计数器初始值是0。

  在main()方法中,分别启动了三个线程。t1通过await()方法等待c1计数器归零,由于c1的初始值是0,t1可以直接执行。t1执行完后,通过countDown()方法将c2计数器减1,此时c2的值为0,t2通过await()方法等待c2计数器归零,此时t2可以执行。t2执行完后,通过countDown()方法将c3计数器减1,此时c3的值为0,t3通过await()方法等待c3计数器归零,此时t3可以执行。整个程序的执行顺序就是t1->t2->t3。

执行结果截图:

思路3【使用单个线程池】

思路3【使用单个线程池】代码示例:

public class Test3 {

    public static void main(String[] args) {
        final Thread t1 = new Thread(() -> System.out.println("thread start T1:"),"T1");
        final Thread t2 = new Thread(() -> System.out.println("thread start T2:"),"T2");
        final Thread t3 = new Thread(() -> System.out.println("thread start T3:"),"T3");

        //创建一个单线程化的线程池,以此保证线程顺序执行.
        ExecutorService executor = Executors.newSingleThreadExecutor();
        
        //执行线程
        executor.submit(t1);
        executor.submit(t2);
        executor.submit(t3);
        //关闭线程池
        executor.shutdown();
    }
}

代码解析:

  在这段代码中,分别创建了三个线程t1、t2、t3,并使用了一个单线程化的线程池ExecutorService。通过调用ExecutorService的submit()方法来将这三个线程提交到线程池中执行。由于线程池是单线程化的,可以保证提交的线程按照提交的顺序执行。最后调用ExecutorService的shutdown()方法来关闭线程池。

执行结果截图:

        综合就是我此次所提供的三种思路,你们会更倾向哪一种实现方式呢?欢迎评论区交流。

        以上三种思路都有各自巧妙之处,对你们而言,还是有很好的借鉴意义,同时也鼓励大家能发挥自己的才能,脑通出更多额解题思路,只要能解题不论方法简单困难,能解题的都是一种突破。

... ...

  ok,以上就是我这期的全部内容啦,如果还想学习更多,你可以看看我的往期热文推荐哦,每天积累一个奇淫小知识,日积月累下去,你一定能成为令人敬仰的大佬。

  「赠人玫瑰,手留余香」,咱们下期拜拜~~

文末💭

我是bug菌,CSDN | 阿里云 | 华为云 | 51CTO 等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金 | InfoQ | 51CTO等社区优质创作者,全网粉丝合计15w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。