likes
comments
collection
share

刚入职的同事问我如何判断线程池任务执行完了?我表示...😐

作者站长头像
站长
· 阅读数 23

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

...

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

🏆本文收录于《Spring Boot从入门到精通》,专门攻坚指数提升。

本专栏致力打造最硬核 Spring Boot 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。

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

1. 前言🔥

        请教同学们个问题哈,现有这样一个场景,在一个复杂的查询逻辑中,无法通过一个sql查询完成该逻辑,需要执行多个查询联合才能实现,然后再将各自查询到的结果集汇总统一处理,那面对这种需求场景,我们优先可能会想到使用线程池其多线程并发执行sql查询,而不是单线程同步执行了,那效率得多慢。

        那么问题来了,你们是如何判断线程池中的任务都全部执行完了再进行下一步操作的呢,对于线程 Thread 而言,很好实现,加一个 join 方法就解决了,但这不就严重影响到该接口的执行耗时。所以呢,你们打算怎么处理?这将又会是干货满满的一期,全程无尿点不废话只抓重点教,具有非常好的学习效果,拿好小板凳准备就坐!希望学习的过程中大家认真听好好学,学习的途中有任何不清楚或疑问的地方皆可评论区留言或私信,bug菌将第一时间给予解惑,那么废话不多说,直接开整!Fighting!! 

刚入职的同事问我如何判断线程池任务执行完了?我表示...😐

2. 环境说明🔥

本地的开发环境:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 1.8
  • Spring Boot版本:2.3.1 RELEASE
  • Maven版本:3.8.2

3. 正文🔥 

3.1 需求分析

        前言提到采用线程池来并发处理多个sql查询,其实使用线程池不麻烦,麻烦的是你要通过什么方式去统计线程池中的任务都被执行,何为都执行完了?其实这也很理解,无非你就是要把握一个点,判断【计划执行任务数】是否等于【已完成任务数】即可,如果相等则说明线程池中的任务全被执行掉了,反之就是未执行完。

        那么你就朝着这个方向去思考,有那些方式可以算出【计划执行任务数】与【已完成任务数】这两个量值?

3.2 实现概述

        统计线程池中的任务是否被全执行完的方法其实有很多很多,我给大家举几个例子: 

  • 使用 getCompletedTaskCount() 统计出【已完成任务数】和使用Java线程池中的getTaskCount() 方法来获取【总任务数】,二者进行对比即可。
  • 使用 FutureTask对象 ,等待所有任务都执行完,线程池的任务就都执行完了。
  • 使用 CountDownLatch对象 或 CyclicBarrier对象,等待所有线程都执行完之后,再执行后续流程,计数。
  • 使用isTerminated() 方法。利用线程池的终止状态(TERMINATED)来判断线程池的任务是否已经全部执行完,但想要线程池的状态发生改变,就需要调用线程池的 shutdown() 方法,不然线程池一直会处于 RUNNING 运行状态,那就没办法使用终止状态来判断任务是否已经全部执行完了,shutdown() 方法是启动线程池有序关闭的方法,它在完全关闭之前会执行完之前所有已经提交的任务,并且不会再接受任何新任务。当线程池中的所有任务都执行完之后,线程池就进入了终止状态,调用 isTerminated() 方法返回的结果就是 true 了,以这点作为依据来判断即可。
  • ...

        如果你有其他的点子,欢迎评论区交流学习。

3.3 实现方案

3.3.1 统计完成已完成任务数

        这里通过使用getCompletedTaskCount()和getTaskCount() 方法分别统计出统计出【已完成任务数】和【总任务数】,如果相等则说明线程池的任务执行完了,否则既未执行完。

示例代码如下:

    //校验计划执行任务数 ?= 已完成任务数
    private static void isCompletedByTaskCount(ThreadPoolExecutor threadPool) {
        while (threadPool.getTaskCount() != threadPool.getCompletedTaskCount()) {
        }
    }

具体演示代码如下:

package com.example.demo.component.threadPool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CountThreadTask {
    //创建一个最大线程数100的线程池
    private static ExecutorService es =
            new ThreadPoolExecutor(3, 100, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(100));


    public static void main(String[] args) throws Exception {
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            es.execute(() -> { //提交执行
                System.out.println("线程" + finalI + "执行完成!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        ThreadPoolExecutor threadPool = ((ThreadPoolExecutor) es);
        System.out.println("线程池任务总数量:"+threadPool.getTaskCount());
        System.out.println("---------线程池开始执行-----------");
        while (true) {
            if (threadPool.getTaskCount() == threadPool.getCompletedTaskCount()) {
                System.out.println("---------线程池执行完了-----------");
                break;
            }
            //间隔2s查询一次
            Thread.sleep(2000);
            System.out.println("线程池还未执行完,敬请等待!已完成的任务数量:"+threadPool.getCompletedTaskCount());
        }
        
    }

}

执行main函数,结果控制台打印示例如下,仅供参考:

刚入职的同事问我如何判断线程池任务执行完了?我表示...😐

方法说明及拓展:

  • getTaskCount():返回线程池计划执行的任务总数。注意:由于任务和线程的状态可能在计算过程中动态变化,因此该方法返回值只是一个近似值,不是精准的。
  • getCompletedTaskCount():返回线程池中已完成的任务数,注意:跟getTaskCount()方法一致,该方法返回值也是一个近似值。
  • getPoolSize():返回线程池当前的线程数量。
  • getActiveCount():返回当前线程池中正在执行任务的线程数量。

方式总结:

        由于getTaskCount() 与 getCompletedTaskCount()方法返回值都是一个近似值而不是精确值,固结果可能有一定的偏差,这也是该方式的一大缺点。

3.3.2 使用 FutureTask 

        与方式1不同的是,FutrueTask 可以弥补它的弊端,使用它可以精准获取任务结果,调用每个 FutrueTask 对象的 get() 方法就是等待该任务执行完,如下代码所示:

package com.example.demo.component.threadPool;

import java.util.concurrent.*;

/**
 * 使用 FutrueTask 等待线程池执行完全部任务
 */
public class FutureTaskTask {
    
    //创建一个最大线程数100的线程池
    private static ExecutorService es =
            new ThreadPoolExecutor(4, 100, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(100));

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 创建任务1
        FutureTask<Integer> task1 = new FutureTask<>(() -> {
            System.out.println("---Task 1 开始执行---");
            Thread.sleep(2000);
            System.out.println("------Task 1 执行结束------");
            return 1;
        });
        // 创建任务2
        FutureTask<Integer> task2 = new FutureTask<>(() -> {
            System.out.println("---Task 2 开始执行---");
            Thread.sleep(3000);
            System.out.println("------Task 2 执行结束------");
            return 2;
        });
        // 创建任务3
        FutureTask<Integer> task3 = new FutureTask<>(() -> {
            System.out.println("---Task 3 开始执行---");
            Thread.sleep(1000);
            System.out.println("------Task 3 执行结束------");
            return 3;
        });
        // 创建任务4
        FutureTask<Integer> task4 = new FutureTask<>(() -> {
            System.out.println("---Task 4 开始执行---");
            Thread.sleep(500);
            System.out.println("------Task 4 执行结束------");
            return 4;
        });
        // 提交4个任务给线程池
        es.submit(task1);
        es.submit(task2);
        es.submit(task3);
        es.submit(task4);

        // 等待所有任务执行完毕
        task1.get();
        task2.get();
        task3.get();
        task3.get();

        //执行完毕
        System.out.println("线程池执行完了!");
    }
}

执行main函数,结果控制台打印示例如下,仅供参考:

刚入职的同事问我如何判断线程池任务执行完了?我表示...😐

3.3.3 使用CountDownLatch 

        CountDownLatch身为同步工具类,作用之一可协调多个线程之间的同步,或者说接通线程之间的通信(而不是互斥)。CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后再继续执行。其中,计数器初始值为全线程的数量,当每一个线程完成自己任务后,计数器的值就会自动减1;当计数器的值 = 0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

        接下来给大家演示下,如何巧妙利用CountDownLatch达到统计线程池所有线程都被执行完的需求?请看示例代码:

package com.example.demo.component.threadPool;

import java.util.concurrent.*;

/**
 * 使用CountDownLatch
 */
public class CountDownLatchTask {

    //创建一个最大线程数100的线程池
    private static ExecutorService es =
            new ThreadPoolExecutor(4, 100, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(100));

    public static void main(String[] args) throws Exception {
        //计数器,判断线程是否执行结束
        //初始值为10
        CountDownLatch taskLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            es.execute(() -> { //提交执行
                taskLatch.countDown();
                System.out.println("当前计数器值为:" + taskLatch.getCount());
                try {
                    //模拟线程执行方法,执行1s
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        //当前线程阻塞,等待计数器置为0
        taskLatch.await();
        System.out.println("线程池执行完了!");
    }


}

执行main函数,结果控制台打印示例如下,仅供参考:

刚入职的同事问我如何判断线程池任务执行完了?我表示...😐

方式总结:

        虽然使用CountDownLatch可达到统计线程是否被执行完,该方式使用起来代码简洁优雅,不需要对线程池进行操作。但由于CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

3.3.4 使用CyclicBarrier 

        CyclicBarrier 和 CountDownLatch 类似,你可以把它理解为一个可以重复使用的循环计数器,CyclicBarrier 可调用 reset() 方法将自己重置到初始状态,这是与CountDownLatch不一样的特性,那具体如何使用CyclicBarrier达到统计线程池所有线程都被执行完的需求吧,具体实现代码如下,仅供参考:

package com.example.demo.component.threadPool;

import java.util.Random;
import java.util.concurrent.*;

/**
 * 使用CyclicBarrier
 */
public class CyclicBarrierTask {

    //创建一个最大线程数100的线程池
    private static ExecutorService es =
            new ThreadPoolExecutor(5, 100, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(100));

    public static void main(String[] args) throws InterruptedException {

        //任务总数
        final int taskCount = 5;
        //循环计数器
        CyclicBarrier cyclicBarrier = new CyclicBarrier(taskCount, new Runnable() {
            @Override
            public void run() {
                // 线程池执行完
                System.out.println("---------线程池执行完了-----------");
            }
        });

        // 添加任务
        for (int i = 0; i < taskCount; i++) {
            final int finalI = i;
            es.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        //随机休眠1-4秒
                        TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                        System.out.println("任务" + finalI + "执行完成");
                        // 线程执行完
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

执行main函数,结果控制台打印示例如下,仅供参考:

刚入职的同事问我如何判断线程池任务执行完了?我表示...😐

3.3.5 使用isTerminated()

        使用线程池的 isTerminated() 方法,在执行 shutdown() 进行线程池的关闭后, 隔间调用isTerminated()判断线程池中的所有任务是否已经完成即可。那具体如何使用 isTerminated() 方法达到统计线程池所有线程都被执行完的需求吧,具体实现代码如下,仅供参考:

package com.example.demo.component.threadPool;

import java.util.concurrent.*;

/**
 * 使用isTerminated()
 */
public class IsTerminatedTask {

    //创建一个最大线程数100的线程池
    private static ExecutorService es =
            new ThreadPoolExecutor(4, 100, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(100));

    public static void main(String[] args) throws Exception {

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            es.execute(() -> { //提交执行
                System.out.println("线程" + finalI + "执行完成!");
                try {
                    //模拟线程执行过程
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        //关闭线程池
        es.shutdown();
        //隔间1s判断是否执行完了,如果所有任务在关闭后完成,返回true。
        while (!es.isTerminated()) {
            Thread.sleep(1000);
        }
        System.out.println("---------线程池执行完了-----------");
    }
}

执行main函数,结果控制台打印示例如下,仅供参考:

刚入职的同事问我如何判断线程池任务执行完了?我表示...😐

在上述代码演示中,在主线程中进行循环判断,全部任务是否已经完成。 

拓展:

  • shutdown() :对线程池进行有序关闭。调用该方法后,线程池将不再接受新的任务,但会继续执行已提交的任务。如果线程池已经处于关闭状态,则对该方法的调用没有额外的作用。
  • isTerminated() :判断线程池中的所有任务是否在关闭后完成。只有在调用了shutdown()或shutdownNow()方法后,所有任务执行完毕,才会返回true。需要注意的是,在调用shutdown()之前调用isTerminated()方法始终返回false值的。

3.4 五种方式优缺点汇总

        如上,我总共诺列了五种解决思路,小伙伴在面对该场景时,猜想第一感觉想到的会是方式1跟方式5吧,但是这五种方式,实现思路上各有优劣,如下bug菌就简单给同学们分析下其中的关系利弊,仅供参考。

3.4.1 使用getCompletedTaskCount()和getTaskCount() 方法

优点:使用它可不需要进行线程池的关闭,避免了创建线程池及销毁所带来的内存开销。

缺点:使用它两方法返回的都是一个近似值,而且进行线程判断局限很大,要保证在循环判断过程中没有产生新的任务,否则该方式就统计失效了。

3.4.2 使用 FutureTask

优点:使用其方法就是主打一个精确值,使用简单优雅,不需要对线程池有任何的操作。

缺点:每个提交给线程池的任务都会关联一个FutureTask对象,这就可能会损耗额外的内存开销。如果需要处理大量的任务,可能会占用较大的内存资源。

3.4.3 使用CountDownLatch 

优点:使用简单优雅,不需要对线程池有任何的操作。

缺点:使用CountDownLatch 计数器只能使用一次,CountDownLatch 创建之后不能重复使用,而且需要提前知道线程的数量,性能较差,还需要在线程代码块内加上异常判断,否则在 countDown()之前发生异常而没有处理,就会导致主线程永远阻塞在 await 。

3.4.4 使用CyclicBarrier 

优点:使用简单优雅,计数器可重置进行重复使用。

缺点:使用难度较高。相比CountDownLatch而言,CyclicBarrier 无论从设计还是使用,复杂度都高于CountDownLatch,相比 CountDownLatch 而言它的优点就是可以重复使用。

3.4.5 使用isTerminated()

优点:使用简单优雅。

缺点:使用场景受限,需要shutdown()关闭线程池。因为日常使用是会将线程池注入到Spring容器里,然后各个组件中都统一用同一个线程池,不能直接关闭线程池。

... ...

        以上提供了五种不同的思路对其进行求解,且分析了这五种方式的使用优劣,希望对同学们有所帮助。如果有小伙伴还有其他的奇思妙想,欢迎评论区大胆交流,一起学习。

... ...

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

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

4. 热文推荐💭

若想学习更多,可以参考这篇专栏总结《2023最新首发,全网最全 Spring Boot 学习宝典(附思维导图)》本专栏致力打造最硬核 Spring Boot 进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中。欢迎大家订阅持续学习。

在入门及进阶之途,我必助你一臂之力,系统性学习,从入门到精通,带你不走弯路,直奔终点;投资自己,永远性价比最高,都这么说了,你还不赶紧来学??

本文涉及所有源代码,均已上传至github开源,供同学们一对一参考 GitHub传送门

同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

5. 文末💭

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

刚入职的同事问我如何判断线程池任务执行完了?我表示...😐

转载自:https://juejin.cn/post/7267826851752656932
评论
请登录