【Java并发杂烩】在并发环境下,如何让主线程感知批量子任务都已经执行完了?

在并发环境下,如果确定任务全部都完成了?


  • 前提概念
    • 背景
    • 要实现的功能
    • 要解决的问题
  • 解决问题
    • 分析问题
    • 解决问题的方案
  • 具体实现
    • 通过CountDownLatch去实现
    • 通过ThreadPoolExecutor.shutdown()和isTerminated()方法去实现
    • 通过ThreadPoolExecutor.getActiveCount()去实现

前提概念


背景

在开发场景中,遇到了一个问题,前端调用我服务端的一个接口,会开启一个耗时任务。但这任务正在执行的过程中,不允许有重复任务再被开启,所以同时间内,只有允许有一个任务在执行,只有任务结束之后,才能接受再次的接口请求。因为这个任务非常耗时,所以是用一个线程池做多线程的并发处理,大概十几分钟酱紫;所以我们的提供的接口必须是一个异步接口,不能让请求一直阻塞着等待服务端处理完后的结果返回,因为这样会造成请求超时;当然我们也无法做到服务端处理完成后主动回调前端,因为HTTP请求并不支持全双工通信。所以架构师要求是当前端请求任务接口,必须做到如果有任务正在执行,则立即返回返回结果,msg : 有任务正在处理中 ;如果任务执行完或没有任务,则可以开启任务,并立即返回结果,msg :正在处理

要实现的功能

  • 接口A用于开启数据处理任务,前端请求服务端后,服务端开启异步任务,无论能否执行,立即返回结果

要解决的问题

所以要做到这个功能,那么我们就要先解决这么几个问题?

  • 当有请求来袭,服务端怎么知道有没有任务正在进行?
  • 当有请求来袭,服务端怎么知道任务已经执行完毕,可以进行下一个任务?(等价于我怎么知道分配到线程池中的并发子任务都执行完了

解决问题


分析问题

  • 问题一: 当有请求来袭,服务端怎么知道有没有任务正在进行?
  • 问题二: 当有请求来袭,服务端怎么知道任务已经执行完毕,可以进行下一个任务?

我们知道要实现上面的功能,那么我们就要先解决上面的两个问题;第一个问题很好解决,一个标志位就可以做到了,比如用一个第三方缓存Redis去解决,当开启任务时,更新Redis的任务状态为执行中,当任务结束,更新Redis中的任务状态为结束删除状态

那么问题来了,好像问题一的实现也是依赖问题二的实现,即我要知道并发任务什么时候被处理完,才能更新任务已完成的状态呀。哈哈,这就回到了标题上的问题啦!到底要怎么知道呢?主线程接到任务通知后,就把每一个子任务都扔进线程池中了,主线程根本就不管我到底是什么时候开始执行,什么时候执行完,凉凉,感觉很苦恼的样子~

解决问题的方案

经过我在网上的查找, 也是发现了一些可行的方案的,所以就在这里跟大家分享一下

  • 通过CountDownLatch去实现
  • 通过ThreadPoolExecutor.shutdown()isTerminated()方法去实现
  • 通过ThreadPoolExecutor.getActiveCount()去实现

具体实现


通过CountDownLatch去实现

public class TaskisDoneTest {


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

		 countDownLatchTest(createExecutor());

    }

  

    private static void countDownLatchTest(ThreadPoolExecutor executor) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        AtomicInteger count = new AtomicInteger(0);

        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                //一定要对子任务的主逻辑进行try/catch, 保证countDownLatch每个子任务执行后都要countDown
                try {
                    doTask(count);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //不管任务是否执行成功,那么都要countDown,避免主线程被永远阻塞或到达超时时间
                    countDownLatch.countDown();
                }

            });
        }
        //主线程阻塞,直至countDownLatch减至0,或超过超时时间
        countDownLatch.await(1, TimeUnit.HOURS);
        System.out.println("任务执行完毕");

    }

    /**
     * 默认耗时子任务
     *
     * @param count
     */
    private static void doTask(AtomicInteger count) throws InterruptedException {
        System.out.println(count.incrementAndGet() + "号子任务正在执行中...");
        Thread.sleep(100);
    }

	/**
	* 获得线程池
	*
	*/
    private static ThreadPoolExecutor createExecutor() {
        //获取系统核心数
        int coreSize = Runtime.getRuntime().availableProcessors();
        int maxSize = coreSize * 4;
        return new ThreadPoolExecutor(coreSize,
                maxSize,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(maxSize),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }


}
  • CountDownLatch的大小要按子任务的数量来定,比如有1000个子任务,那就需要将CountDownLatch的大小初始化为1000
  • 子线程获取CountDownLatch,每次执行完任务后就countDown(), 相当于-1操作
  • 但要注意的是,子线程操作CountDownLatch时,要特别关注,任务是可能会出现异常情况的,所以通常我们要将子线程主逻辑try/catch/finally起来,将countDown()操作放到finally中。这样可以做到即使子任务出现异常,也可以正确执行countDown()操作,避免造成死锁
  • 主线程将所有子任务抛进线程池后,就可以执行await()等待,为了安全起见,我们最好先预估整个并发任务的时间,给予主线程最大的超时等待时间,避免出现死锁

通过ThreadPoolExecutor.shutdown()和isTerminated()方法去实现

public class TaskisDoneTest {


    public static void main(String[] args) throws InterruptedException {
        executorShutDownTest(createExecutor());
    }


    private static void executorShutDownTest(ThreadPoolExecutor executor) throws InterruptedException {
        AtomicInteger count = new AtomicInteger(0);
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                try {
                    doTask(count);
                } catch (Exception e) {
                    e.getStackTrace();
                }
            });
        }

        //关闭线程池,线程池发出关闭指令,不再接收新的任务,但不会立即关闭,会将剩余任务执行完后再关闭(在阻塞队列中的任就会执行的)
        executor.shutdown();
        while (true) {
            //判断线程池是否已关闭,如果已关闭,就代表所有任务执行完毕了
            if (executor.isTerminated()) {
                System.out.println("任务执行完毕");
                break;
            }
            //每次判断休眠200毫秒,避免长时间空转
            Thread.sleep(200);
        }
    }


    /**
     * 默认耗时子任务
     *
     * @param count
     */
    private static void doTask(AtomicInteger count) throws InterruptedException {
        System.out.println(count.incrementAndGet() + "号子任务正在执行中...");
        Thread.sleep(100);
    }


    private static ThreadPoolExecutor createExecutor() {
        //获取系统核心数
        int coreSize = Runtime.getRuntime().availableProcessors();
        int maxSize = coreSize * 4;
        return new ThreadPoolExecutor(coreSize,
                maxSize,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(maxSize),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }


}

原理:

  • 当我们对线程池执行shutdown()操作了之后,线程池的阻塞队列就不会再接受新的任务了,正在执行任务的线程没有执行完,且在阻塞队列中的任务没有被线程执行完前,线程池依然是不会关闭的。只有线程池中所有的线程都执行完毕,阻塞队列已无任务的时候,线程池才会得以关闭。所有线程池是可以感知被提交到池中的任务是什么时候被全部执行完的
  • 当我们对线程池执行isTerminated()操作时,是可以感知线程池是否已经关闭,如果关闭就代表所有被投递到线程池中的任务都已经被执行完了。

代码:

  • 在所有的子任务都被投递到线程池后,就可以开始对线程池进行shutdown操作了,但注意的是,不是shutdownNow操作,它们两者是不一样的
  • 当线程池执行完shutdown操作后,我们就可以轮询主线程,不断判断线程池是否已经关闭,isTerminated()

通过ThreadPoolExecutor.getActiveCount()去实现

public class TaskisDoneTest {


    public static void main(String[] args) throws InterruptedException {
        executorActiveTest(createExecutor());
    }

    private static void executorActiveTest(ThreadPoolExecutor executor) throws InterruptedException {
        AtomicInteger count = new AtomicInteger(0);
        //将1000个子任务投递到线程池中
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                try {
                    doTask(count);
                } catch (Exception e) {
                    e.getStackTrace();
                }
            });
        }

        while (true){
        	//线程池中的活跃数是否为0,
            if (executor.getActiveCount() == 0){
                System.out.println("任务执行完毕");
                break;
            }
            //每次判断休眠200毫秒,避免长时间空转
            Thread.sleep(200);
        }
    }

    /**
     * 默认耗时子任务
     *
     * @param count
     */
    private static void doTask(AtomicInteger count) throws InterruptedException {
        System.out.println(count.incrementAndGet() + "号子任务正在执行中...");
        Thread.sleep(100);
    }


    private static ThreadPoolExecutor createExecutor() {
        //获取系统核心数
        int coreSize = Runtime.getRuntime().availableProcessors();
        int maxSize = coreSize * 4;
        return new ThreadPoolExecutor(coreSize,
                maxSize,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(maxSize),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }



原理:

  • 线程池的getActiveCount操作可以获知,线程池当前正活跃中的线程个数(近似值);如果线程池中的活跃线程数趋近于0,就说明投递到线程池中的子任务都已经被执行完了
  • Returns the approximate number of threads that are actively executing tasks.

代码:

  • 先将所有的子任务都投递到线程池中,然后主线程轮询判断线程池中的活跃子线程是否趋近于0,如果是就代表所有子任务已完成

小结

  • CountDownLatch的方式比较常见,不失为一种控制线程逻辑的好方式,不过要是操作不当,容易操作线程死锁的问题。
  • 线程池shutdown的方式比较简单,只需要等待线程池关闭即可,实现简单,每次的请求都会创建一个新的线程池去执行任务,然后在关闭它,具有一定的线程池内存消耗
  • 线程池getActiveCount的方法的返回值是一个近似值,只能保证一个大约情况。因为自己没有这么使用过,而且可能会存在其他影响因子动态的变化,所以这个方法不是太推荐,但是在一定程度上也可以做到。重点是它实现简单,而已不需要每次都重建线程池
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章