资讯详情

boot2.X教程4,定时任务,Elastic job 分片 防止冲突,异步,线程池配置和隔离 拒绝策略,日志管理 logback...

1. 使用@Scheduled实现定时任务

例如:我需要定期发送短信、电子邮件等操作,也可以定期检查和监控一些标志、参数等。

创建定时任务

在Spring Boot中编写定时任务是非常简单的事,下面通过实例介绍如何在Spring Boot创建定时任务,每5秒输出当前时间。

开启注解

  • 在Spring Boot加入主类@EnableScheduling注释,启用定时任务的配置
@SpringBootApplication @EnableScheduling public class Application { 
           public static void main(String[] args) { 
           SpringApplication.run(Application.class, args);  }  } 
  • 创建定时任务实现类

具体实现类

@Component public class ScheduledTasks { 
              private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");      @Scheduled(fixedRate = 5000)     public void reportCurrentTime() { 
                 log.info("现在时间:"   dateFormat.format(new Date()));     }  } 
  • 在控制台中可以看到类似于以下输出的操作程序,定时任务开始正常运行。

@Scheduled详解

在上述入门实例中使用@Scheduled(fixedRate = 5000) 注释定义每5秒执行的任务。@Scheduled让我们从源码中看看配置:

@Target({ 
        ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled { 
        

	String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;

	String cron() default "";

	String zone() default "";

	long fixedDelay() default -1;

	String fixedDelayString() default "";

	long fixedRate() default -1;

	String fixedRateString() default "";

	long initialDelay() default -1;

	String initialDelayString() default "";

}

这些具体配置信息的含义如下:

  • cron:通过cron表达式来配置执行规则

  • zone:cron表达式解析时使用的时区

  • fixedDelay:上一次执行结束到 下一次执行开始的间隔时间(单位:ms)

  • fixedDelayString:上一次任务执行结束到 下一次执行开始的间隔时间,使用java.time.Duration#parse解析

  • fixedRate:以固定间隔执行任务,即上一次任务执行开始到下一次执行开始的间隔时间(单位:ms),若在调度任务执行时,上一次任务还未执行完毕,会加入worker队列,等待上一次执行完成后立即执行下一次任务

  • fixedRateString:与fixedRate逻辑一致,只是使用java.time.Duration#parse解析

  • initialDelay:首次任务执行的延迟时间

  • initialDelayString:首次任务执行的延迟时间,使用java.time.Duration#parse解析

  • 每7秒执行一次(定时任务没5秒一次,真实需要7秒,下次任务会在 worker队列卡着,等7秒后上次执行完毕,直接执行)

@Scheduled(fixedRate = 5000)
public void reportCurrentTime() { 
        
        try { 
        
            Thread.sleep(7000L);
        } catch (Exception e) { 
        

        }
}

思考与进阶

这种模式实现的定时任务缺少在集群环境下的协调机制。

什么意思呢?假设,我们要实现一个定时任务,用来每天网上统计某个数据然后累加到原始数据上。我们开发测试的时候不会有问题,因为都是单进程在运行的。

  • 但是,当我们把这样的定时任务部署到生产环境时,为了更高的可用性,启动多个实例是必须的。
  • 此时,时间一到,所有启动的实例就会同时开始执行这个任务。
    • 那么问题也就出现了,因为有累加操作,最终我们的结果就会出现问题。

解决这样问题的方式很多种,比较通用的就是采用分布式的方式,

  • 让同类任务之前的时候以分布式锁的方式来控制执行顺序,
  • 比如:使用Redis、Zookeeper等具备分布式锁功能的中间件配合就能很好的帮助我们来协调这类任务在集群模式下的执行规则。

本文的完整工程可以查看下面仓库中的chapter7-1目录:

2. 使用Elastic Job实现定时任务

当在集群环境下的时候,如果任务的执行或操作依赖一些共享资源的话,就会存在竞争关系。如果不引入分布式锁等机制来做调度的话,就可能出现预料之外的执行结果。

所以,@Scheduled注解更偏向于使用在单实例自身维护相关的一些定时任务上会更为合理一些,

  • 比如:定时清理服务实例某个目录下的文件、定时上传本实例的一些统计数据等。

Elastic Job

Elastic Job的前生是当当开源的一款分布式任务调度框架,而目前已经加入到了Apache基金会。

该项目下有两个分支:ElasticJob-Lite和ElasticJob-Cloud。

  • ElasticJob-Lite是一个轻量级的任务管理方案,本文接下来的案例就用这个来实现。
  • 而 ElasticJob-Cloud则相对重一些,因为它使用容器来管理任务和隔离资源。

更多关于ElasticJob的介绍,您也可以点击这里直达官方网站了解更多信息。

动手试试

引入pom

pom.xml中添加elasticjob-lite的starter

<dependencies>
    <dependency>
        <groupId>org.apache.shardingsphere.elasticjob</groupId>
        <artifactId>elasticjob-lite-spring-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>

    // ...
</dependencies>

创建简单的任务

:创建一个简单任务

@Slf4j
@Service
public class MySimpleJob implements SimpleJob {

    @Override
    public void execute(ShardingContext context) {
        log.info("MySimpleJob start : didispace.com {}", System.currentTimeMillis());
    }

}

配置文件

:编辑配置文件

elasticjob.reg-center.server-lists=localhost:2181
elasticjob.reg-center.namespace=didispace

elasticjob.jobs.my-simple-job.elastic-job-class=com.didispace.chapter72.MySimpleJob
elasticjob.jobs.my-simple-job.cron=0/5 * * * * ?
elasticjob.jobs.my-simple-job.sharding-total-count=1

这里主要有两个部分:

第一部分:elasticjob.reg-center开头的,主要配置elastic job的注册中心和namespace

第二部分:任务配置,以elasticjob.jobs开头,

  • 这里的my-simple-job是任务的名称,根据你的喜好命名即可,但不要重复。
  • 任务的下的配置elastic-job-class是任务的实现类,cron是执行规则表达式,
  • sharding-total-count是任务分片的总数。我们可以通过这个参数来把任务切分,实现并行处理。这里先设置为1,后面我们另外讲分片的使用。

运行与测试

这里需要用到ZooKeeper来协调分布式环境下的任务调度。所以,你需要先在本地安装ZooKeeper,然后启动它。

  • 注意:上面elasticjob.reg-center.server-lists配置,
  • 根据你实际使用的ZooKeeper地址和端口做相应修改。

在启动上述Spring Boot应用之后,我们可以看到如下日志输出:

didispace.com 1626766435013

既然是分布式任务调度,那么我们再启动一个(注意,在同一台机器启动的时候,会端口冲突,可以在启动命令中加入-Dserver.port=8081来区分端口),在第二个启动的服务日志也打印了类似的内容

此时,在回头看看之前第一个启动的应用,日志输出停止了。由于我们设置了分片总数为1,所以这个任务启动之后,只会有一个实例接管执行。

  • 这样就避免了多个进行同时重复的执行相同逻辑而产生问题的情况。
  • 同时,这样也支持了任务执行的高可用。比如:可以尝试把第二个启动的应用(正在打印日志的)终止掉。可以发现,第一个启动的应用(之前已经停止输出日志)继续开始打印任务日志了。

在整个实现过程中,我们并没有自己手工的去编写任何的分布式锁等代码去实现任务调度逻辑,只需要关注任务逻辑本身,然后通过配置分片的方式来控制任务的分割,就可以轻松的实现分布式集群环境下的定时任务管理了。是不是在复杂场景下,这种方式实现起来要比@Scheduled更方便呢?

本文的完整工程可以查看下面仓库中的chapter7-2目录:

3. 使用Elastic Job的分片配置

就是;同时,为了实现定时任务的高可用,还启动了很多任务实例,但,资源利用率不高。

所以,接下来我们就来继续介绍,使用Elastic Job的分片配置,来为任务执行加加速,资源利用抬抬高的目标!

  • 结果:每个实例都在跑,只是 分支不同。提高执行效率

动手试试

建议直接下载文末仓库中的chapter7-2工程,然后在这个基础上进行修改。当然,如果你对如何使用Elastic Job还不输入,那么先前往上一篇做个知识铺垫,再继续下面的内容!

:创建一个分片执行的任务

@Slf4j
@Service
public class MyShardingJob implements SimpleJob { 
        

    @Override
    public void execute(ShardingContext context) { 
        
        switch (context.getShardingItem()) { 
        
            case 0:
                log.info("分片1:执行任务");
                break;
            case 1:
                log.info("分片2:执行任务");
                break;
            case 2:
                log.info("分片3:执行任务");
                break;
        }
    }

}

这里通过switch来判断当前任务上下文的sharding-item值来执行不同的分片任务。

  • sharding-item的值取决于后面将要配置的分片总数,但注意是从0开始计数的。
  • 这里仅采用了日志打印的方式,来展示分片效果,
  • 真正实现业务逻辑的时候,
    • 一定记得根据分片数量对执行任务也要做分片操作的设计。
    • 比如:你可以根据批量任务的id求摩的方式来区分不同分片处理不同的数据,以避免重复执行而出现问题。

:在配置文件中,设置配置任务的实现类、执行表达式、以及将要重要测试的分片总数参数

elasticjob.jobs.my-sharding-job.elastic-job-class=com.didispace.chapter73.MyShardingJob
elasticjob.jobs.my-sharding-job.cron=0/5 * * * * ?
elasticjob.jobs.my-sharding-job.sharding-total-count=3

这里设置为3,所以任务会被分为3个分片,每个分片对应第一步中一个switch的分支。

运行与测试

我们可以看到,每间隔5秒,这个实例会打印这样的日志:

 分片1:执行任务
2021-07-21 17:42:05.254  INFO 63478 --- [-sharding-job-3] com.didispace.chapter73.MyShardingJob    
: 分片3:执行任务
: 分片2:执行任务
: 分片1:执行任务
: 分片2:执行任务
: 分片3:执行任务

每次任务都被拆分成了3个分片任务,就如我上文中所说的,每个分片对应一个switch的分支。由于当前情况下,我们只启动了一个实例,所以3个分片任务都被分配到了这个唯一的实例上。

接下来,我们再启动一个实例(注意使用-Dserver.port来改变不同的端口,不然本地会启动不成功)。此时,两个实例的日志出现了变化:

实例1的日志:

分片2:执行任务
分片2:执行任务
分片2:执行任务

实例2的日志:

分片1:执行任务
分片3:执行任务
分片1:执行任务
分片3:执行任务

随着实例数量的增加,可以看到分片的分配发生了变化。这也就意味着,当一个任务开始执行的时候,两个任务执行实例都被利用了起来,这样我们的任务执行和资源利用的效率就可以得到优化。

你也可以尝试再继续启动实例和关闭实例来观察任务的动态分配,怎么样?这样写定时任务是不是方便多了?

本文的完整工程可以查看下面仓库中的chapter7-3目录:

4. 使用Elastic Job的namespace防止任务名冲突

这篇《使用Elastic Job实现定时任务》文章编写测试定时任务的时候,报了类似下面的这个错误:

Job conflict with register center. The job 'my-simple-job' in register center's class is 'com.didispace.chapter72.MySimpleJob', your job class is 'com.didispace.chapter74.MySimpleJob'

根据错误消息Job conflict with register center. The job 'my-simple-job' in register center's,初步判断是ZooKeeper中存储的任务配置出现冲突:任务名一样,但实现类不同。

经过一番交流,原来他是使用公司测试环境的ZooKeeper来写的例子做测试,同时之前有同事(也是DD的读者)也写过类似的任务,因为配置的任务名称是拷贝的,所以出现了任务名称相对,但实现类不同的情况。

实际上,如果我们在一个大一些的团队做开发的时候,只要存在多系统的话,那么定时任务的重名其实是很有可能发生。比如:很多应用都可能存在一些定时清理某些资源的任务,就很可能起一样的名字,然后注册到同一个ZooKeeper,最后出现冲突。 那么有什么好办法来解决这个问题吗?

方法一:任务创建的统一管理

最原始的处理方法,就是集中的管理任务创建流程,比如:可以开一个Wiki页面,所有任务在这个页面上登记,每个人登记的时候,可以查一下想起的名字是否已经存在。如果存在了就再想一个名字,并做好登记。

这种方法很简单,也很好理解。但存在的问题是,当任务非常非常多的时候,这个页面内容就很大,维护起来也是非常麻烦的。

方法二:namespace属性来隔离

  • 巧用Elastic Job的namespace属性来隔离任务名称

回忆一下之前第一篇写定时任务的时候,关于注册中心的配置是不是有下面两项:

elasticjob.reg-center.server-lists=localhost:2181
elasticjob.reg-center.namespace=didispace

第一个elasticjob.reg-center.server-lists不多说,就是ZooKeeper的访问地址。这里要重点讲的就是第二个参数elasticjob.reg-center.namespace

其实在ZooKeeper中注册任务的时候,真正冲突的并不纯粹是因为任务名称,

  • 而是namespace + 任务名称,全部一样,才会出现问题。

所以,我们只需要把每个应用创建的任务都隔离在自己独立的namespace里,那么是不是就不会和其他应用出现冲突了呢?

最后,我给出了下面这样的建议:

spring.application.name=chapter74

elasticjob.reg-center.server-lists=localhost:2181
elasticjob.reg-center.namespace=${spring.application.name}

即:将定时任务服务的elasticjob.reg-center.namespace设置为当前Spring Boot应用的名称一致spring.application.name

通常,我们在规划各个Spring Boot应用的时候,都会做好唯一性的规划,这样未来注册到Eureka、Nacos等注册中心的时候,也可以保证唯一。

利用好这个唯一参数,也可以方便的帮我们把各个应用的定时任务也都隔离出来,也就解决了文章开头,我们所说的场景了。

本文的完整工程可以查看下面仓库中的chapter7-4目录:

5. 使用@Async实现异步调用

什么是“异步调用”?“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。

同步调用

下面通过一个简单示例来直观的理解什么是同步调用:

定义Task类,创建三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10秒内)

@Slf4j
@Component
public class AsyncTasks { 
        

    public static Random random = new Random();

    public void doTaskOne() throws Exception { 
        
        log.info("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务一,耗时:" + (end - start) + "毫秒");
    }

    public void doTaskTwo() throws Exception { 
        
        log.info("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务二,耗时:" + (end - start) + "毫秒");
    }

    public void doTaskThree() throws Exception { 
        
        log.info("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务三,耗时:" + (end - start) + "毫秒");
    }

}

在单元测试用例中,注入Task对象,并在测试用例中执行doTaskOnedoTaskTwodoTaskThree三个函数。

@Slf4j
@SpringBootTest
public class Chapter75ApplicationTests { 
        

    @Autowired
    private AsyncTasks asyncTasks;

    @Test
    public void test() throws Exception { 
        
        asyncTasks.doTaskOne();
        asyncTasks.doTaskTwo();
        asyncTasks.doTaskThree();
    }

}

执行单元测试,可以看到类似如下输出:

开始做任务一
完成任务一,耗时:4865毫秒

开始做任务二
完成任务二,耗时:7063毫秒

开始做任务三
完成任务三,耗时:2076毫秒

任务一、任务二、任务三顺序的执行完了,换言之doTaskOne、doTaskTwo、doTaskThree三个函数顺序的执行完成。

异步调用

上述的同步调用虽然顺利的执行完了三个任务,但是可以看到执行时间比较长,若这三个任务本身之间不存在依赖关系,可以并发执行的话,同步调用在执行效率方面就比较差,可以考虑通过异步调用的方式来并发执行。

在Spring Boot中,我们只需要通过使用@Async注解就能简单的将原来的同步函数变为异步函数,Task类改在为如下模式:

使用@Async

@Slf4j
@Component
public class AsyncTasks { 
        

    public static Random random = new Random();

    @Async
    public void doTaskOne() throws Exception { 
        
    }
    
}

开启异步调用

为了让@Async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsync,如下所示:

@EnableAsync
@SpringBootApplication
public class Chapter75Application { 
        

    public static void main(String[] args) { 
        
        SpringApplication.run(Chapter75Application.class, args);
    }

}

此时可以反复执行单元测试,您可能会遇到各种不同的结果,比如:

  • 没有任何任务相关的输出
  • 有部分任务相关的输出
  • 乱序的任务相关的输出
  • 原因是目前doTaskOnedoTaskTwodoTaskThree三个函数的时候已经是异步执行了。主程序在异步调用之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就自动结束了,导致了不完整或是没有输出任务相关内容的情况。

注:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效

异步回调

为了让doTaskOnedoTaskTwodoTaskThree能正常结束,假设我们需要统计一下三个任务并发执行共耗时多少,这就需要等到上述三个函数都完成调动之后记录时间,并计算结果。

那么我们如何判断上述三个异步调用是否已经执行完成呢?我们需要使用CompletableFuture<T>来返回异步调用的结果,就像如下方式改造doTaskOne函数:

completedFuture方法创建返回

@Async
public CompletableFuture<String> doTaskOne() throws Exception { 
        
    log.info("开始做任务一");
    long start = System.currentTimeMillis();
    Thread.sleep(random.nextInt(10000));
    long end = System.currentTimeMillis();
    log.info("完成任务一,耗时:" + (end - start) + "毫秒");
    return CompletableFuture.completedFuture("任务一完成");
}

按照如上方式改造一下其他两个异步函数之后,下面我们改造一下测试用例,让测试在等待完成三个异步调用之后来做一些其他事情。

join() 方法等待 执行完毕

    @Autowired
    private AsyncTasks asyncTasks;

@Test
public void test() throws Exception { 
        
    long start = System.currentTimeMillis();

    CompletableFuture<String> task1 = asyncTasks.doTaskOne();
    CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
    CompletableFuture<String> task3 = asyncTasks.doTaskThree();

    CompletableFuture.allOf(task1, task2, task3).join();

    long end = System.currentTimeMillis();

    log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
}

看看我们做了哪些改变:

  • 在测试用例一开始记录开始时间
  • 在调用三个异步函数的时候,返回CompletableFuture<String>类型的结果对象
  • 通过CompletableFuture.allOf(task1, task2, task3).join()实现三个异步任务都结束之前的阻塞效果
  • 三个任务都完成之后,根据结束时间 - 开始时间,计算出三个任务并发执行的总耗时。

执行一下上述的单元测试,可以看到如下结果:

开始做任务三
开始做任务二
开始做任务一
完成任务二,耗时:6312毫秒
完成任务三,耗时:8465毫秒
完成任务一,耗时:8560毫秒
任务全部完成,总耗时:8590毫秒

可以看到,通过异步调用,让任务一、二、三并发执行,有效的减少了程序的总运行时间。

本文的完整工程可以查看下面仓库中2.x目录下的chapter7-5工程:

6. 配置@Async异步任务的线程池

上一篇我们介绍了如何使用@Async注解来创建异步任务,我可以用这种方法来实现一些并发操作,以加速任务的执行效率。但是,如果只是如前文那样直接简单的创建来使用,可能还是会碰到一些问题。存在有什么问题呢?先来思考下,下面的这个接口,通过异步任务加速执行的实现,是否存在问题或风险呢?

@RestController
public class HelloController { 
        

    @Autowired
    private AsyncTasks asyncTasks;
        
    @GetMapping("/hello")
    public String hello() { 
        
			//如上
    }
}

虽然,从单次接口调用来说,是没有问题的。但当接口被客户端频繁调用的时候,异步任务的数量就会大量增长:3 x n(n为请求数量),如果任务处理不够快,就很可能会出现内存溢出的情况。那么为什么会内存溢出呢?根本原因是由于Spring Boot默认用于异步任务的线程池是这样配置的:

img

图中我标出的两个重要参数是需要关注的:

  • queueCapacity:缓冲队列的容量,默认为INT的最大值(2的31次方-1)。
  • maxSize:允许的最大线程数,默认为INT的最大值(2的31次方-1)。

所以,默认情况下,一般任务队列就可能把内存给堆满了。所以,我们真正使用的时候,还需要对异步任务的执行线程池做一些基础配置,以防止出现内存溢出导致服务不可用的问题。

配置默认线程池

默认线程池的配置很简单,只需要在配置文件中完成即可,主要有以下这些参数:

spring.task.execution.pool.core-size=2  #始化线程数,默认为8
spring.task.execution.pool.max-size=5  #最大线程数,默认为int最大值

spring.task.execution.pool.queue-capacity=10 #缓冲执行任务的队列,默认为int最大值
spring.task.execution.pool.keep-alive=60s	#存活60秒 保持空闲的时间

spring.task.execution.pool.allow-core-thread-timeout=true	#是否允许核心线程超时

spring.task.execution.shutdown.await-termination=false	#是否等待剩余任务完成后才关闭应用
spring.task.execution.shutdown.await-termination-period=	#等待剩余任务完成的最大时间

spring.task.execution.thread-name-prefix=task-		#线程的前缀

具体配置含义如下:

capacity 
英 /kəˈpæsəti/  美 /kəˈpæsəti/  全球(英国)  
简明 牛津 新牛津  韦氏  柯林斯 例句  百科
n. 能力,才能;容积,容纳能力;职位,职责;功率,容积;生产量,生产能力
adj. 无虚席的,满场的
复数 capacities

termination 
英 /ˌtɜːmɪˈneɪʃ(ə)n/  美 /ˌtɜːrmɪˈneɪʃn/  全球(英国)  
简明 牛津 新牛津  韦氏  例句  百科
n. 人工流产;结束,终止;<美>解聘,解雇;<美>暗杀;词尾(尤指屈折变化或派生词的词尾);<古>结局

period 
英 /ˈpɪəriəd/  美 /ˈpɪriəd/  全球(美国)  
简明 牛津 新牛津  韦氏  柯林斯 例句  百科
n. 一段时间,时期;(人生或国家历史的)阶段,时代;(地质年代划分的)纪;课时,节;(练习、训练或学习的)时段;(体育比赛的)局;<美>句号,句点;(物理)(振动或循环的)周期;(天文)自转(或公转)周期;(数学)(周期函数的)周期;(化学)周期元素;(修辞)完整句;(乐)乐段,乐节
adj. 具有某个时代特征的
adv. <美>到此为止,不再说了

具体配置含义如下:

  • spring.task.execution.pool.core-size:线程池创建时的初始化线程数,默认为8
  • spring.task.execution.pool.max-size:线程池的最大线程数,默认为int最大值
  • spring.task.execution.pool.queue-capacity:用来缓冲执行任务的队列,默认为int最大值
  • spring.task.execution.pool.keep-alive:线程终止前允许保持空闲的时间
  • spring.task.execution.pool.allow-core-thread-timeout:是否允许核心线程超时
  • spring.task.execution.shutdown.await-termination:是否等待剩余任务完成后才关闭应用
  • spring.task.execution.shutdown.await-termination-period:等待剩余任务完成的最大时间
  • spring.task.execution.thread-name-prefix:线程名的前缀,设置好了之后可以方便我们在日志中查看处理任务所在的线程池

动手试一试

默认8个线程,同时执行

由于默认线程池的核心线程数是8,所以3个任务会同时开始执行,日志输出是这样的:

开始做任务二
开始做任务三
开始做任务一
完成任务二,耗时:672毫秒
完成任务三,耗时:4677毫秒
完成任务一,耗时:5624毫秒
任务全部完成,总耗时:5653毫秒

核心2个,缓冲10个,每次执行2个

接着,可以尝试在配置文件中增加如下的线程池配置

spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10

日志输出的顺序会变成如下的顺序:

开始做任务一
开始做任务二

完成任务一,耗时:2439毫秒
开始做任务三

完成任务二,耗时:5867毫秒
完成任务三,耗时:7894毫秒

任务全部完成,总耗时:10363毫秒
  • 任务一和任务二会马上占用核心线程,任务三进入队列等待
  • 任务一完成,释放出一个核心线程,任务三从队列中移出,并占用核心线程开始处理

注意:这里可能有的小伙伴会问,最大线程不是5么,为什么任务三是进缓冲队列,不是创建新线程来处理吗?

这里要理解缓冲队列与最大线程间的关系:

  • 只有在缓冲队列 满了之后才会 申请超过核心线程数的线程 来进行处理。
  • 所以,这里只有缓冲队列中10个任务满了,再来第11个任务的时候,才会在线程池中创建第三个线程来处理。
  • 读者可以自己调整下参数,或者调整下单元测试来验证这个逻辑。

本文的完整工程可以查看下面仓库中2.x目录下的chapter7-6工程:

4个任务 核心2个,只执行2个

如:

spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=2
#开四个,只能同时执行2个,会有2个排队
CompletableFuture.allOf(task1, task2, task3,task4).join();
        
开始做任务一
开始做任务二
完成任务一,耗时:598毫秒
开始做任务三

核心满了,触发最大线程数

capacity:用来缓冲执行任务的队列,改为1,能同时执行3个。因为缓冲队列满了,会创建线程(<最大的线程5个)

开始做任务一
开始做任务二
开始做任务四
完成任务四,耗时:1357毫秒

7. 如何隔离@Async异步任务的线程池

过上一篇:配置@Async异步任务的线程池的介绍,你应该已经了解到异步任务的执行背后有一个线程池来管理执行任务。为了控制异步任务的并发不影响到应用的正常运作,我们必须要对线程池做好相应的配置,防止资源的过渡使用。除了默认线程池的配置之外,还有一类场景,也是很常见的,那就是多任务情况下的线程池隔离。

什么是线程池的隔离,为什么要隔离

可能有的小伙伴还不太了解?。所以,我们先来看看下面的场景案例:

@RestController
public class HelloController 
        标签: 2290连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台