一、@Scheduled 注解简介
@Scheduled 是 Spring 框架中的一个注解,它能够用于配置定时工作,使得办法能够依照规定的工夫距离定时执行。在应用该注解时,咱们能够指定工作的执行工夫、循环周期、并发数等参数,从而实现定时工作的性能。在 Spring Boot 中,@Scheduled 注解能够间接利用于办法上。
二、@Scheduled 的多线程机制
在 Spring Boot 中,@Scheduled 注解是基 f 于 Java 的 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 实现的。当咱们配置了一个定时工作后,Spring Boot 会首先创立一个 ScheduledThreadPoolExecutor 线程池,并将定时工作增加到该线程池中期待执行。而后,在指定的工夫到来之后,线程池会为该定时任务分配一个线程来执行。如果该定时工作还未执行结束,在下一个周期达到时,线程池会为该工作再次调配一个线程来执行。通过这种形式,@Scheduled 能够十分不便地实现周期性的定时工作 f 于 Java 的 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 实现的。当咱们配置了一个定时工作后,Spring Boot 会首先创立一个 ScheduledThreadPoolExecutor 线程池,并将定时工作增加到该线程池中期待执行。而后,在指定的工夫到来之后,线程池会为该定时任务分配一个线程来执行。如果该定时工作还未执行结束,在下一个周期达到时,线程池会为该工作再次调配一个线程来执行。通过这种形式,@Scheduled 能够十分不便地实现周期性的定时工作。
三、@Scheduled 的多线程问题
尽管 @Scheduled 注解十分便捷,然而它也存在一些多线程的问题,次要体现在以下两个方面:
定时工作未执行结束时,后续工作可能会受到影响
在应用 @Scheduled 注解时,咱们很容易疏忽一个问题:如果定时工作在执行时,下一个周期的工作曾经到了,那么后续工作可能会受到影响。例如,咱们定义了一个间隔时间为 5 秒的定时工作 A,在第 1 秒时开始执行,须要执行 10 秒钟。在第 6 秒时,定时工作 A 还没有完结,此时下一个周期的工作 B 曾经开始期待执行。如果此时线程池中没有足够的闲暇线程,那么定时工作 B 就会被阻塞,无奈执行。
多个定时工作并发执行可能导致资源竞争
在某些状况下,咱们可能须要编写多个定时工作,这些定时工作可能波及到共享资源,例如数据库连贯、缓存对象等。当多个定时工作同时执行时,就会存在资源竞争的问题,可能会导致数据谬误或者零碎解体。
四、@Scheduled 退出线程池来解决定时工作
为了防止上述问题,能够将 @Scheduled 工作交给线程池进行解决。在 Spring Boot 中,能够通过以下两种形式来将 @Scheduled 工作退出线程池:
应用 @EnableScheduling + @Configuration 配置 ThreadPoolTaskScheduler
@Configuration
@EnableScheduling
public class TaskSchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.initialize();
return scheduler;
}
}
复制代码
在上述代码中,咱们通过配置 ThreadPoolTaskScheduler 来创立一个线程池,并应用 @EnableScheduling 注解将定时工作开启。其中,setPoolSize 办法能够设置线程池的大小,默认为 1。
应用 ThreadPoolTaskExecutor
@Configuration
@EnableScheduling
public class TaskExecutorConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("task-executor-");
return executor;
}
}
复制代码
在上述代码中,咱们通过配置 ThreadPoolTaskExecutor 来创立一个线程池,并应用 @EnableScheduling 注解将定时工作开启。其中 setCorePoolSize、setMaxPoolSize、setQueueCapacity、setKeepAliveSeconds 等办法能够用于配置线程池的大小和工作队列等参数。
五、@Scheduled 详细分析
在 Spring Boot 中,@Scheduled 注解是基于 Java 的 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 实现的。当咱们配置了一个定时工作后,Spring Boot 会首先创立一个 ScheduledThreadPoolExecutor 线程池,并将定时工作增加到该线程池中期待执行。而后,在指定的工夫到来之后,线程池会为该定时任务分配一个线程来执行。如果该定时工作还未执行结束,在下一个周期达到时,线程池会为该工作再次调配一个线程来执行。通过这种形式,@Scheduled 能够十分不便地实现周期性的定时工作。
尽管 @Scheduled 注解十分便捷,然而它也存在一些多线程的问题,次要体现在以下两个方面:
- 定时工作未执行结束时,后续工作可能会受到影响
在应用 @Scheduled 注解时,咱们很容易疏忽一个问题:如果定时工作在执行时,下一个周期的工作曾经到了,那么后续工作可能会受到影响。例如,咱们定义了一个间隔时间为 5 秒的定时工作 A,在第 1 秒时开始执行,须要执行 10 秒钟。在第 6 秒时,定时工作 A 还没有完结,此时下一个周期的工作 B 曾经开始期待执行。如果此时线程池中没有足够的闲暇线程,那么定时工作 B 就会被阻塞,无奈执行。
解决方案:
针对上述问题,咱们能够采纳以下两种计划来解决:
计划一:批改线程池大小
为了防止因为线程池中线程数量有余引起的问题,咱们能够对线程池进行配置,进步线程池的大小,从而确保有足够的闲暇线程来解决定时工作。
例如,咱们能够在 application.properties 或 application.yml 或者应用 @EnableScheduling + @Configuration 来配置线程池大小:
spring.task.scheduling.pool.size=20
复制代码 - 多个定时工作并发执行可能导致资源竞争
在某些状况下,咱们可能须要编写多个定时工作,这些定时工作可能波及到共享资源,例如数据库连贯、缓存对象等。当多个定时工作同时执行时,就会存在资源竞争的问题,可能会导致数据谬误或者零碎解体。
解决方案:
为了防止因为多个定时工作并发执行导致的资源竞争问题,咱们能够采纳以下两种计划来解决:
计划一:应用锁机制
锁机制是一种常见的解决多线程并发访问共享资源的形式。在 Java 中,咱们能够应用 synchronized 关键字或者 Lock 接口来实现锁机制。
例如,上面是一个应用 synchronized 关键字实现锁机制的示例:
private static Object lockObj = new Object();
@Scheduled(fixedDelay = 1000)
public void doSomething(){
synchronized(lockObj){// 定时工作要执行的内容}
}
复制代码
在上述代码中,咱们定义了一个动态对象 lockObj,用来爱护共享资源。在定时工作执行时,咱们应用 synchronized 关键字对 lockObj 进行加锁,从而确保多个定时工作不能同时访问共享资源。
计划二:应用分布式锁
除了应用传统的锁机制外,还能够应用分布式锁来解决资源竞争问题。分布式锁是一种基于分布式系统的锁机制,它能够不依赖于单个 JVM 实例,从而可能保障多个定时工作之间的资源拜访不会抵触。
在 Java 开发中,咱们能够应用 ZooKeeper、Redis 等分布式系统来实现分布式锁机制。例如,应用 Redis 实现分布式锁的示例代码如下:
@Autowired
private RedisTemplate redisTemplate;
@Scheduled(fixedDelay = 1000)
public void doSomething(){
String lockKey = "lock:key";
String value = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, value, 5L, TimeUnit.SECONDS);
if(result){
try{// 定时工作要执行的内容}finally{redisTemplate.delete(lockKey);
}
}
}
复制代码
在上述代码中,咱们应用 Redis 实现了分布式锁机制。具体而言,咱们在定时工作执行时,首先向 Redis 中写入一个键值对,而后查看是否胜利写入。如果胜利写入,则示意以后定时工作取得了锁,能够执行接下来的操作。在定时工作执行结束后,咱们再从 Redis 中删除该键值对,开释锁资源。
六、总结
通过以上的剖析,咱们能够理解到:尽管 @Scheduled 注解可能十分不便地实现定时工作的性能,然而它也存在一些多线程的问题。为此,须要留神到这些问题,并采取相应的措施来防止它们的呈现。在理论开发中,能够联合应用线程池、异步线程池、锁机制、分布式锁等形式,达到最佳的成果。