一、@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 {

@Beanpublic TaskScheduler taskScheduler() {    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();    scheduler.setPoolSize(10);    scheduler.initialize();    return scheduler;}

}
复制代码
在上述代码中,咱们通过配置ThreadPoolTaskScheduler来创立一个线程池,并应用@EnableScheduling注解将定时工作开启。其中,setPoolSize办法能够设置线程池的大小,默认为1。

应用ThreadPoolTaskExecutor

@Configuration
@EnableScheduling
public class TaskExecutorConfig {

@Beanpublic 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注解十分便捷,然而它也存在一些多线程的问题,次要体现在以下两个方面:

  1. 定时工作未执行结束时,后续工作可能会受到影响
    在应用@Scheduled注解时,咱们很容易疏忽一个问题:如果定时工作在执行时,下一个周期的工作曾经到了,那么后续工作可能会受到影响。例如,咱们定义了一个间隔时间为5秒的定时工作A,在第1秒时开始执行,须要执行10秒钟。在第6秒时,定时工作A还没有完结,此时下一个周期的工作B曾经开始期待执行。如果此时线程池中没有足够的闲暇线程,那么定时工作B就会被阻塞,无奈执行。
    解决方案:
    针对上述问题,咱们能够采纳以下两种计划来解决:
    计划一:批改线程池大小
    为了防止因为线程池中线程数量有余引起的问题,咱们能够对线程池进行配置,进步线程池的大小,从而确保有足够的闲暇线程来解决定时工作。
    例如,咱们能够在application.properties或application.yml或者应用@EnableScheduling + @Configuration来配置线程池大小:
    spring.task.scheduling.pool.size=20
    复制代码
  2. 多个定时工作并发执行可能导致资源竞争
    在某些状况下,咱们可能须要编写多个定时工作,这些定时工作可能波及到共享资源,例如数据库连贯、缓存对象等。当多个定时工作同时执行时,就会存在资源竞争的问题,可能会导致数据谬误或者零碎解体。
    解决方案:
    为了防止因为多个定时工作并发执行导致的资源竞争问题,咱们能够采纳以下两种计划来解决:
    计划一:应用锁机制
    锁机制是一种常见的解决多线程并发访问共享资源的形式。在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注解可能十分不便地实现定时工作的性能,然而它也存在一些多线程的问题。为此,须要留神到这些问题,并采取相应的措施来防止它们的呈现。在理论开发中,能够联合应用线程池、异步线程池、锁机制、分布式锁等形式,达到最佳的成果。