带你了解控制线程执行顺序的几种方法

通常情况下,线程的执行顺序都是随机的,哪个获取到CPU的时间片,哪个就获得执行的机会。不过实际的项目中有时我们会有需要不同的线程顺序执行的需求。借助一些java中的线程阻塞和同步机制,我们往往也可以控制多个线程的执行顺序。

方法有很多种,本篇文章介绍几种常用的。

利用 thread join实现线程顺序执行

thread.join方法的可以实现如下的效果,就是挂起调用join方法的线程的执行,直到被调用的线程执行结束。听起来有点绕,举个例子解释下:

假设有t1, t2两个线程,如果在t2的线程流程中调用了 t1.join, 那么t2线程将会停止执行,等待t1执行结束后才会继续执行。

很显然,利用这个机制,我们可以控制线程的执行顺序,看下面的例子:


public class ControlThreadDemo {

    public static void main(String[] args) {

        Thread previousThread = Thread.currentThread();

        for (int i = 0; i < 10; i++) {
            ThreadJoinDemo threadJoinDemo = new ThreadJoinDemo(previousThread);
            threadJoinDemo.start();
            previousThread = threadJoinDemo;
        }

        System.out.println("主线程执行完毕");

    }
}


public class ThreadJoinDemo extends Thread{

    private Thread previousThread;
    public ThreadJoinDemo(Thread thread) {
        this.previousThread = thread;
    }

    public void run() {
        try {
            System.out.println("线程:" + Thread.currentThread().getName() + " 等待 " + previousThread.getName());
            previousThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "开始执行");
    }


}

运行结果:

线程:Thread-1 等待 Thread-0
线程:Thread-5 等待 Thread-4
线程:Thread-4 等待 Thread-3
线程:Thread-3 等待 Thread-2
线程:Thread-2 等待 Thread-1
线程:Thread-0 等待 main
线程:Thread-8 等待 Thread-7
线程:Thread-7 等待 Thread-6
线程:Thread-6 等待 Thread-5
线程:Thread-9 等待 Thread-8
主线程执行完毕
Thread-0开始执行
Thread-1开始执行
Thread-2开始执行
Thread-3开始执行
Thread-4开始执行
Thread-5开始执行
Thread-6开始执行
Thread-7开始执行
Thread-8开始执行
Thread-9开始执行

从执行结果可以很容易理解, 程序运行起来之后,一共11个线程排好队等着执行,排在最前面的是 main 线程,然后依次是t0, t1 ...。

利用 CountDownLatch 控制线程的执行顺序

还是先说下 CountDownLatch 的用法,CountDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行。借用一张经典的图:

在这里插入图片描述

CountDownLatch提供两个核心的方法,countDown和await,后者可以阻塞调用它的线程, 而前者每调用一次,计数器减去1,当计数器减到0的时候,阻塞的线程被唤醒继续执行。

场景1

先看一个例子,在这个例子中,主线程会等有若干个子线程执行完毕之后再执行,不过这若干个子线程之间的执行顺序是随机的。

public class ControlThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        List<Thread> workers = Stream.generate(() -> new Thread(new CountDownDemo(countDownLatch))).limit(5).collect(Collectors.toList());
        workers.forEach(Thread::start);
        countDownLatch.await();

        System.out.println("主线程执行完毕");

    }
}


public class CountDownDemo implements Runnable{
    private CountDownLatch countDownLatch;

    public CountDownDemo(CountDownLatch latch) {
        this.countDownLatch = latch;
    }

    @Override
    public void run() {
        System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
        countDownLatch.countDown();
    }
}

输出,

线程Thread-0开始执行
线程Thread-3开始执行
线程Thread-2开始执行
线程Thread-1开始执行
线程Thread-4开始执行
主线程执行完毕

这种场景在实际项目中有需要的场景,比如我之前看过一个案例,大概的场景是说需要下载一个大文件,开启多个线程分别下载文件的一部分,然后有一个线程最后拼接所有的文件。我们可以考虑使用 CountDownLatch 来控制并发,使拼接的线程放在最后执行。

场景2

这个案例带你了解下利用 CountDownLatch 控制一组线程一起执行。就好像在运动场上,教练的发令枪一响,所有运动员一起跑。我们一般在模拟线程并发执行的时候会用到这种场景。

我们把场景1的代码稍微改造一下,

public class ControlThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch readyLatch = new CountDownLatch(5);
        CountDownLatch runningLatchWait = new CountDownLatch(1);
        CountDownLatch completeLatch = new CountDownLatch(5);


        List<Thread> workers = Stream.generate(() -> new Thread(new CountDownDemo2(readyLatch,runningLatchWait,completeLatch))).limit(5).collect(Collectors.toList());
        workers.forEach(Thread::start);
        readyLatch.await();//等待发令
        runningLatchWait.countDown();//发令
        completeLatch.await();//等所有子线程执行完

        System.out.println("主线程执行完毕");

    }
}

public class CountDownDemo2 implements Runnable{
    private CountDownLatch readyLatch;
    private CountDownLatch runningLatchWait;
    private CountDownLatch completeLatch;

    public CountDownDemo2(CountDownLatch readyLatch, CountDownLatch runningLatchWait, CountDownLatch completeLatch) {
        this.readyLatch = readyLatch;
        this.runningLatchWait = runningLatchWait;
        this.completeLatch = completeLatch;
    }

    @Override
    public void run() {
        readyLatch.countDown();
        try {
            runningLatchWait.await();
            System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            completeLatch.countDown();
        }
    }
}

场景3

到这里,可能很多人会想问,利用 CountDownLatch 能做到像前面thread.join控制多个线程按照一个固定的先后顺序执行吗?

首先我要说,用 CountDownLatch 实现这种场景确实不多见,不过也不是不可以做。请继续看场景3。

public class ControlThreadDemo {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch first = new CountDownLatch(1);
        CountDownLatch prev = first;

        for (int i = 0; i < 10; i++) {
            CountDownLatch next = new CountDownLatch(1);
            new CountDownDemo3(prev, next).start();
            prev = next;
        }
        first.countDown();
    }
}


public class CountDownDemo3 extends Thread{
    private CountDownLatch prev;
    private CountDownLatch next;

    public CountDownDemo3(CountDownLatch prev, CountDownLatch next) {
        this.prev = prev;
        this.next = next;
    }

    @Override
    public void run() {
        try {
            prev.await();
            Thread.sleep(1000);//模拟线程执行耗时
            System.out.println("线程" + Thread.currentThread().getName() + "开始执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            next.countDown();
        }
    }
}

输出,

线程Thread-0开始执行
线程Thread-1开始执行
线程Thread-2开始执行
线程Thread-3开始执行
线程Thread-4开始执行
线程Thread-5开始执行
线程Thread-6开始执行
线程Thread-7开始执行
线程Thread-8开始执行
线程Thread-9开始执行

代码也不难理解,for循环里把10个线程串联起来,排好队等着执行。排在最前面的线程t1在等first这个计数器countDown,然后t1开始执行,执行完调用自己的next计数器 countDown 以唤醒下一个,依次类推。

利用 newSingleThreadExecutor 控制线程的执行顺序

java的 Executors 线程池平时工作中用得很多了,JAVA通过Executors提供了四种线程池,单线程化线程池(newSingleThreadExecutor)、可控最大并发数线程池(newFixedThreadPool)、可回收缓存线程池(newCachedThreadPool)、支持定时与周期性任务的线程池(newScheduledThreadPool)。

顾名思义,newSingleThreadExecutor 线程池只有一个线程。它存在的意义就在于控制线程执行的顺序,保证任务的执行顺序和提交顺序一致。其实保证顺序执行的原理也很简单,因为总是只有一个线程处理任务队列上的任务,先提交的任务必将被先处理。

废话不多说,上代码。

public static void main(String[] args) throws InterruptedException {
        final ExecutorService executorService = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 5; i++){
            final int index = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    Thread.currentThread().setName("thread-" + index);
                    System.out.println("线程: " + Thread.currentThread().getName() + " 开始执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        executorService.awaitTermination(30, TimeUnit.SECONDS);
        executorService.shutdownNow();
    }

输出,

线程: thread-0 开始执行
线程: thread-1 开始执行
线程: thread-2 开始执行
线程: thread-3 开始执行
线程: thread-4 开始执行
线程: thread-5 开始执行
线程: thread-6 开始执行
线程: thread-7 开始执行
线程: thread-8 开始执行
线程: thread-9 开始执行

参考


关注公众号:犀牛饲养员的技术笔记

个人博客:http://www.machengyu.net

csdn博客: https://blog.csdn.net/pony_ma...

思否: https://segmentfault.com/u/ma...


犀牛饲养员的技术笔记
这个世界上有好人,也有坏人,更多是不好不坏的人。我努力去做一个好人。一直从事java后端开发,架构方...
263 声望
270 粉丝
0 条评论
推荐阅读
总结一些ES不常用的filter
ES内置的token filter很多,大部分实际工作中都用不到。这段时间准备ES认证工程师的考试,备考的时候需要熟悉这些不常用的filter。ES官方对一些filter只是一笔带过,我就想着把备考的笔记整理成博客备忘,也希望...

犀牛饲养员阅读 2.5k

谈JVM参数GC线程数ParallelGCThreads合理性设置
在讲这个参数之前,先谈谈JVM垃圾回收(GC)算法的两个优化标的:吞吐量和停顿时长。JVM会使用特定的GC收集线程,当GC开始的时候,GC线程会和业务线程抢占CPU时间,吞吐量定义为CPU用于业务线程的时间与CPU总消耗时...

京东云开发者3阅读 351

封面图
浅析数据库多表连接:开务数据库的分布式 join 计算
Join 是 SQL 中的常用操作。在实际的数据库应用中,我们经常需要从多个数据表中读取数据,这时我们就可以使用 SQL 语句中的连接(join),在两个或多个数据表中查询数据。

KaiwuDB阅读 1.2k

封面图
请求量突增一下,系统有效QPS为何下降很多?
最近我观察到一个现象,当服务的请求量突发的增长一下时,服务的有效QPS会下降很多,有时甚至会降到0,这种现象网上也偶有提到,但少有解释得清楚的,所以这里来分享一下问题成因及解决方案。

扣钉日记阅读 831

封面图
Runable和Callable的区别?你必须要搞清楚Thread以及FutureTask!
Runable与Callable的区别,据说是高频面试题,什么样的答案才会让面试官满意呢?所有java程序员都知道的答案是基于: {代码...} {代码...} 从Runable和Callable接口的定义我们就能知道的是:Runable接口的run方法...

阅读 568

7000字+24张图带你彻底弄懂线程池
今天跟大家聊一聊无论是在工作中常用还是在面试中常问的线程池,通过画图的方式来彻底弄懂线程池的工作原理,以及在实际项目中该如何自定义适合业务的线程池。

码上小康阅读 547

为什么说IO密集型业务,线程数是CPU数的2倍?
也不知道这是哪本书的坑爹理论,现在总有一些小青年老拿着这样的定理来说教。说的信誓旦旦,毋庸置疑,仿佛是权威的化身。讨论时把这样的理论当作前提,​真的是受害不浅。

Java架构师阅读 358

263 声望
270 粉丝
宣传栏