新来个阿里p7,仅花2小时,撸出一个多线程永动任务,看完直接跪了,真牛逼!...

HollisChuang HollisChuang     2022-10-23     730

关键词:

大家好,今天教大家撸一个 Java 的多线程永动任务,这个示例的原型是公司自研的多线程异步任务项目,我把里面涉及到多线程的代码抽离出来,然后进行一定的改造。

里面涉及的知识点非常多,特别适合有一定工作经验的同学学习,或者可以直接拿到项目中使用。

文章结构非常简单:

1. 功能说明

做这个多线程异步任务,主要是因为我们有很多永动的异步任务,什么是永动呢?就是任务跑起来后,需要一直跑下去。

比如消息 Push 任务,因为一直有消息过来,所以需要一直去消费 DB 中的未推送消息,就需要整一个 Push 的永动异步任务。

我们的需求其实不难,简单总结一下:

  1. 能同时执行多个永动的异步任务

  2. 每个异步任务,支持开多个线程去消费这个任务的数据;

  3. 支持永动异步任务的优雅关闭,即关闭后,需要把所有的数据消费完毕后,再关闭。

完成上面的需求,需要注意几个点:

  1. 每个永动任务,可以开一个线程去执行;

  2. 每个子任务,因为需要支持并发,需要用线程池控制;

  3. 永动任务的关闭,需要通知子任务的并发线程,并支持永动任务和并发子任务的优雅关闭

2. 多线程任务示例

2.1 线程池

对于子任务,需要支持并发,如果每个并发都开一个线程,用完就关闭,对资源消耗太大,所以引入线程池:

public class TaskProcessUtil 
    // 每个任务,都有自己单独的线程池
    private static Map<String, ExecutorService> executors = new ConcurrentHashMap<>();

    // 初始化一个线程池
    private static ExecutorService init(String poolName, int poolSize) 
        return new ThreadPoolExecutor(poolSize, poolSize,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(),
                new ThreadFactoryBuilder().setNameFormat("Pool-" + poolName).setDaemon(false).build(),
                new ThreadPoolExecutor.CallerRunsPolicy());
    

    // 获取线程池
    public static ExecutorService getOrInitExecutors(String poolName,int poolSize) 
        ExecutorService executorService = executors.get(poolName);
        if (null == executorService) 
            synchronized (TaskProcessUtil.class) 
                executorService = executors.get(poolName);
                if (null == executorService) 
                    executorService = init(poolName, poolSize);
                    executors.put(poolName, executorService);
                
            
        
        return executorService;
    

    // 回收线程资源
    public static void releaseExecutors(String poolName) 
        ExecutorService executorService = executors.remove(poolName);
        if (executorService != null) 
            executorService.shutdown();
        
    

这是一个线程池的工具类,这里初始化线程池和回收线程资源很简单,我们主要讨论获取线程池。

获取线程池可能会存在并发情况,所以需要加一个 synchronized 锁,然后锁住后,需要对 executorService 进行二次判空校验。

2.2 单个任务

为了更好讲解单个任务的实现方式,我们的任务主要就是把 Cat 的数据打印出来,Cat 定义如下:

@Data
@Service
public class Cat 
    private String catName;
    public Cat setCatName(String name) 
        this.catName = name;
        return this;
    

单个任务主要包括以下功能:

  • 获取永动任务数据:这里一般都是扫描 DB,我直接就简单用 queryData() 代替。

  • 多线程执行任务:需要把数据拆分成 4 份,然后分别由多线程并发执行,这里可以通过线程池支持;

  • 永动任务优雅停机:当外面通知任务需要停机,需要执行完剩余任务数据,并回收线程资源,退出任务;

  • 永动执行:如果未收到停机命令,任务需要一直执行下去。

直接看代码:

public class ChildTask 

    private final int POOL_SIZE = 3; // 线程池大小
    private final int SPLIT_SIZE = 4; // 数据拆分大小
    private String taskName;

    // 接收jvm关闭信号,实现优雅停机
    protected volatile boolean terminal = false;

    public ChildTask(String taskName) 
        this.taskName = taskName;
    

    // 程序执行入口
    public void doExecute() 
        int i = 0;
        while(true) 
            System.out.println(taskName + ":Cycle-" + i + "-Begin");
            // 获取数据
            List<Cat> datas = queryData();
            // 处理数据
            taskExecute(datas);
            System.out.println(taskName + ":Cycle-" + i + "-End");
            if (terminal) 
                // 只有应用关闭,才会走到这里,用于实现优雅的下线
                break;
            
            i++;
        
        // 回收线程池资源
        TaskProcessUtil.releaseExecutors(taskName);
    

    // 优雅停机
    public void terminal() 
        // 关机
        terminal = true;
        System.out.println(taskName + " shut down");
    

    // 处理数据
    private void doProcessData(List<Cat> datas, CountDownLatch latch) 
        try 
            for (Cat cat : datas) 
                System.out.println(taskName + ":" + cat.toString() + ",ThreadName:" + Thread.currentThread().getName());
                Thread.sleep(1000L);
            
         catch (Exception e) 
            System.out.println(e.getStackTrace());
         finally 
            if (latch != null) 
                latch.countDown();
            
        
    

    // 处理单个任务数据
    private void taskExecute(List<Cat> sourceDatas) 
        if (CollectionUtils.isEmpty(sourceDatas)) 
            return;
        
        // 将数据拆成4份
        List<List<Cat>> splitDatas = Lists.partition(sourceDatas, SPLIT_SIZE);
        final CountDownLatch latch = new CountDownLatch(splitDatas.size());

        // 并发处理拆分的数据,共用一个线程池
        for (final List<Cat> datas : splitDatas) 
            ExecutorService executorService = TaskProcessUtil.getOrInitExecutors(taskName, POOL_SIZE);
            executorService.submit(new Runnable() 
                @Override
                public void run() 
                    doProcessData(datas, latch);
                
            );
        

        try 
            latch.await();
         catch (Exception e) 
            System.out.println(e.getStackTrace());
        
    

    // 获取永动任务数据
    private List<Cat> queryData() 
        List<Cat> datas = new ArrayList<>();
        for (int i = 0; i < 5; i ++) 
            datas.add(new Cat().setCatName("罗小黑" + i));
        
        return datas;
    

简单解释一下:

  • queryData:用于获取数据,实际应用中其实是需要把 queryData 定为抽象方法,然后由各个任务实现自己的方法。

  • doProcessData:数据处理逻辑,实际应用中其实是需要把 doProcessData 定为抽象方法,然后由各个任务实现自己的方法。

  • taskExecute:将数据拆分成 4 份,获取该任务的线程池,并交给线程池并发执行,然后通过 latch.await() 阻塞。当这 4 份数据都执行成功后,阻塞结束,该方法才返回。

  • terminal:仅用于接受停机命令,这里该变量定义为 volatile,所以多线程内存可见;

  • doExecute:程序执行入口,封装了每个任务执行的流程,当 terminal=true 时,先执行完任务数据,然后回收线程池,最后退出。

2.3 任务入口

直接上代码:

public class LoopTask 
    private List<ChildTask> childTasks;
    public void initLoopTask() 
        childTasks = new ArrayList();
        childTasks.add(new ChildTask("childTask1"));
        childTasks.add(new ChildTask("childTask2"));
        for (final ChildTask childTask : childTasks) 
            new Thread(new Runnable() 
                @Override
                public void run() 
                    childTask.doExecute();
                
            ).start();
        
    
    public void shutdownLoopTask() 
        if (!CollectionUtils.isEmpty(childTasks)) 
            for (ChildTask childTask : childTasks) 
                childTask.terminal();
            
        
    
    public static void main(String args[]) throws Exception
        LoopTask loopTask = new LoopTask();
        loopTask.initLoopTask();
        Thread.sleep(5000L);
        loopTask.shutdownLoopTask();
    

每个任务都开一个单独的 Thread,这里我初始化了 2 个永动任务,分别为 childTask1 和 childTask2,然后分别执行,后面 Sleep 了 5 秒后,再关闭任务,我们可以看看是否可以按照我们的预期优雅退出。

2.4 结果分析

执行结果如下:

childTask1:Cycle-0-Begin
childTask2:Cycle-0-Begin
childTask1:Cat(catName=罗小黑0),ThreadName:Pool-childTask1
childTask1:Cat(catName=罗小黑4),ThreadName:Pool-childTask1
childTask2:Cat(catName=罗小黑4),ThreadName:Pool-childTask2
childTask2:Cat(catName=罗小黑0),ThreadName:Pool-childTask2
childTask1:Cat(catName=罗小黑1),ThreadName:Pool-childTask1
childTask2:Cat(catName=罗小黑1),ThreadName:Pool-childTask2
childTask2:Cat(catName=罗小黑2),ThreadName:Pool-childTask2
childTask1:Cat(catName=罗小黑2),ThreadName:Pool-childTask1
childTask2:Cat(catName=罗小黑3),ThreadName:Pool-childTask2
childTask1:Cat(catName=罗小黑3),ThreadName:Pool-childTask1
childTask2:Cycle-0-End
childTask2:Cycle-1-Begin
childTask1:Cycle-0-End
childTask1:Cycle-1-Begin
childTask2:Cat(catName=罗小黑0),ThreadName:Pool-childTask2
childTask2:Cat(catName=罗小黑4),ThreadName:Pool-childTask2
childTask1:Cat(catName=罗小黑4),ThreadName:Pool-childTask1
childTask1:Cat(catName=罗小黑0),ThreadName:Pool-childTask1
childTask1 shut down
childTask2 shut down
childTask2:Cat(catName=罗小黑1),ThreadName:Pool-childTask2
childTask1:Cat(catName=罗小黑1),ThreadName:Pool-childTask1
childTask1:Cat(catName=罗小黑2),ThreadName:Pool-childTask1
childTask2:Cat(catName=罗小黑2),ThreadName:Pool-childTask2
childTask1:Cat(catName=罗小黑3),ThreadName:Pool-childTask1
childTask2:Cat(catName=罗小黑3),ThreadName:Pool-childTask2
childTask1:Cycle-1-End
childTask2:Cycle-1-End

输出数据:

  • “Pool-childTask” 是线程池名称;

  • “childTask” 是任务名称;

  • “Cat(catName=罗小黑)” 是执行的结果;

  • “childTask shut down” 是关闭标记;

  • “childTask:Cycle-X-Begin” 和“childTask:Cycle-X-End” 是每一轮循环的开始和结束标记。

我们分析一下执行结果:

  • childTask1 和 childTask2 分别执行,在第一轮循环中都正常输出了 5 条罗小黑数据;

  • 第二轮执行过程中,我启动了关闭指令,这次第二轮执行没有直接停止,而是先执行完任务中的数据,再执行退出,所以完全符合我们的优雅退出结论。

2.5 源码地址

GitHub 地址:

https://github.com/lml200701158/java-study/tree/master/src/main/java/com/java/parallel/pool/ofc

3. 写在最后

对于这个经典的线程池使用示例,原项目是我好友一灰写的,技术水平阿里 P7级别,实现得也非常优雅,涉及的知识点非常多,非常值得大家学习。

新来个p7被辞退了,因为写代码从来不打日志……

-作为一名新生代农民工的我,在对这一官方认证称号领旨谢恩的同时,也承受着来自亲人朋友甚至更多人的误解……在很多人的眼里,程序员的工作就是对着电脑发发呆、写写代码就行了,他们不知道,我们... 查看详情

新来个p7被辞退了,因为写代码从来不打日志……(代码片段)

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇作者丨了不起的程序员来源丨了不起的程序员(ID:great_developer)-作为一名新生代农民工的我,在对这一官方认证称号领旨谢恩的同时&... 查看详情

新来个p7被辞退了,因为写代码从来不打日志……

-作为一名新生代农民工的我,在对这一官方认证称号领旨谢恩的同时,也承受着来自亲人朋友甚至更多人的误解……在很多人的眼里,程序员的工作就是对着电脑发发呆、写写代码就行了,他们不知道,我们... 查看详情

新来个技术总监要我做一个ip属地功能~

Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记!文章来源:juejin.cn/post/7118954784853327903目录背景HttpServletRequest获取IPIp2regionIp2regionV2.0特性ip2regionxdbjava查询客户端实现IDEA中做个测试背景细心的朋友应该会发... 查看详情

一份阿里p7的面试题

...人多,普通的程序员也会受到影响,提升的更快。下面是阿里P7的面试题,Java多线程线程池的原理,为什么要创建线程池?线程的生命周期,什么时候会出现僵死进程;什么实现线程安全,如何实现线程安全;创建线程池有哪几... 查看详情

阿里p7面试题及答案

Java多线程线程池的原理,为什么要创建线程池?线程的生命周期,什么时候会出现僵死进程;什么实现线程安全,如何实现线程安全;创建线程池有哪几个核心参数?如何合理配置线程池的大小?synchronized、volatile区别、synchroni... 查看详情

java一个线程处理多个任务

...处理十个任务,因为每个任务的执行时间是很长的(超过半小时),有没有可能让一条线程同时执行两个任务,比如任务一执行5秒后将任务1暂停,然后执行任务2,执行5秒后再切换到任务1,这样循环直到两个任务都结束.java可以实现这样的... 查看详情

新来个技术总监,禁止我们使用lombok!(代码片段)

...,在一家小型互联网公司做Java后端开发,最近他们公司新来了一个技术总监,这位技术总监对技术细节很看重,一来公司之后就推出了很多"政策",比如定义了很多开发规范、日志规范、甚至是要求大家统一使用某一款ID... 查看详情

多线程

...同时执行。Java的重要功能之一就是内部支持多线程,在一个程序中运行同时运行多个任务。线程:一个程序可能包含多个同时运行的任务。线程是指一个任务从头至尾的执行流程。在java中,每个任务都是Runnable接口的一个实例... 查看详情

新来个技术总监,把rabbitmq讲的那叫一个透彻,佩服!

整体阅读时间,在40分钟左右。常见的消息队列很多,主要包括RabbitMQ、Kafka、RocketMQ和ActiveMQ,这篇文章只讲RabbitMQ,先讲原理,后搞实战。文章很长,如果你能一次性看完,“大哥,请收下我的膝... 查看详情

多线程

 1基本概念01进程进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。02线程2-1基本概念1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程)... 查看详情

多任务爬虫(代码片段)

...2、多任务分类进程:进程是操作资源分配的最小单位,一个运行的程序,至少包括一个进程,进程之间数据不能共享。(利用多核)线程:线程是cpu调度的最小单位,一个进程中至少含有一个线程,线程中数据是共享的,如果... 查看详情

多线程基础

...行的程序,具有一定的独立功能。线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中也可以有多个线程,这个程序被称为所线程程序。简而言之:一个程序运行后至少有... 查看详情

python多任务之多线程开发(代码片段)

...锁的概念5.2互斥锁的使用5.4使用互斥锁完成2个线程对同一个全局变量各加100万次的操作5.3小结6死锁6.1死锁的概念6.2死锁示例6.3避免死锁6.4小结7进程和线程的对比7.1进程和线程的对比的三个方向7.2关系对比7.3区别对比7.4优缺点对... 查看详情

java多线程基础

1.多进程与多线程多进程:每个独立执行的任务就是一个进程操作系统将时间划分为多个时间片,在每个时间片内将CPU分配给某一个任务,时间片结束,CPU将自动回收,再分配给其他任务多进程的缺点:比较笨重不好切换多线程... 查看详情

阿里p8架构师总结java并发面试题(精选)

一、什么是线程?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个... 查看详情

python核心编程总结(五多任务编程之进程与线程)(代码片段)

...务编程我们之前所写的程序都是单任务的,也就是说一个函数或者方法执行完成另外一个函数或者方法才能执行,要想实现这种操作就需要使用多任务。多任务的最大好处是充分利用CPU资源,提高程序的执行效率。1.1... 查看详情

python核心编程总结(五多任务编程之进程与线程)(代码片段)

...务编程我们之前所写的程序都是单任务的,也就是说一个函数或者方法执行完成另外一个函数或者方法才能执行,要想实现这种操作就需要使用多任务。多任务的最大好处是充分利用CPU资源,提高程序的执行效率。1.1... 查看详情