1. @Scheduled创建定时任务
- https://blog.didispace.com/spring-boot-learning-1x/
创建定时任务
@SpringBootApplication
@EnableScheduling
public class Application {
}
@Component
public class ScheduledTasks {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
System.out.println("现在时间:" + dateFormat.format(new Date()));
}
}
现在时间:20:38:59
现在时间:20:39:04
现在时间:20:39:09
现在时间:20:39:14
现在时间:20:39:19
@Scheduled详解
@Scheduled(fixedRate = 5000)
注解来定义每过5秒执行的任务,
如下几种方式:
@Scheduled(fixedRate = 5000)
:上一次开始执行时间点之后5秒再执行@Scheduled(fixedDelay = 5000)
:上一次执行完毕时间点之后5秒再执行@Scheduled(initialDelay=1000, fixedRate=5000)
:第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次@Scheduled(cron="*/5 * * * * *")
:通过cron表达式定义规则
2. @Async实现异步调用
什么是“异步调用”?
“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;
异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。
异步调用
若这三个任务本身之间不存在依赖关系,可以并发执行的话,同步调用在执行效率方面就比较差
只需要通过使用@Async
注解就能简单的将原来的同步函数变为异步函数
//统一加在方法上,也可以
@Component
public class Task {
@Async
public void doTaskOne() throws Exception {
// 同上内容,省略
}
@Async
public void doTaskTwo() throws Exception {
// 同上内容,省略
}
}
让@Async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsync
开始做任务一
开始做任务二
开始做任务三
完成任务二,耗时:1795毫秒
完成任务三,耗时:4556毫秒
完成任务一,耗时:6702毫秒
开始做任务二
开始做任务三
开始做任务一
完成任务二,耗时:220毫秒
完成任务一,耗时:5811毫秒
完成任务三,耗时:6714毫秒
配置
@SpringBootApplication
@EnableAsync//默认是开启的
public class Application {
}
主程序在异步调用之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就自动结束了,导致了不完整或是没有输出任务相关内容的情况。
注: @Async所修饰的函数不要定义为static类型,这样异步调用不会生效
基本的类
@Component
public class Task {
public static Random random =new Random();
public void doTaskOne() throws Exception {
System.out.println("开始做任务一");
//记录开始时间
long start = System.currentTimeMillis();
//睡眠随机10秒内的数
Thread.sleep(random.nextInt(10000));
//记录结束时间
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
}
//doTaskTwo()
//doTaskThree()
}
开始做任务一
完成任务一,耗时:6894毫秒
开始做任务二
完成任务二,耗时:8335毫秒
开始做任务三
完成任务三,耗时:5835毫秒
异步回调
- 接口返回:Future
- 真实返回:new AsyncResult<>(“任务一完成”);
为了让doTaskOne
、doTaskTwo
、doTaskThree
能正常结束,假设我们需要统计一下三个任务并发执行共耗时多少,这就需要等到上述三个函数都完成调动之后记录时间,并计算结果。
如何判断上述三个异步调用是否已经执行完成呢?我们需要使用Future<T>
来返回异步调用的结果,就像如下方式改造doTaskOne
函数:
@Async
public Future<String> doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务一完成");
}
改造一下测试用例,让测试在等待完成三个异步调用之后来做一些其他事情。
long start = System.currentTimeMillis();
Future<String> task1 = task.doTaskOne();
Future<String> task2 = task.doTaskTwo();
Future<String> task3 = task.doTaskThree();
while(true) {
if(task1.isDone() && task2.isDone() && task3.isDone()) {
// 三个任务都调用完成,退出循环等待
break;
}
Thread.sleep(1000);
}
long end = System.currentTimeMillis();
System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");
看看我们做了哪些改变:
- 在测试用例一开始记录开始时间
- 在调用三个异步函数的时候,返回
Future<String>
类型的结果对象 - 在调用完三个异步函数之后,开启一个循环,根据返回的
Future<String>
对象来判断三个异步函数是否都结束了。若都结束,就结束循环;若没有都结束,就等1秒后再判断。 - 跳出循环之后,根据结束时间 - 开始时间,计算出三个任务并发执行的总耗时。
执行一下上述的单元测试,可以看到如下结果:
开始做任务一
开始做任务二
开始做任务三
完成任务三,耗时:37毫秒
完成任务二,耗时:3661毫秒
完成任务一,耗时:7149毫秒
任务全部完成,总耗时:8025毫秒
3. @Async实现异步调用:自定义线程池
通过自定义线程池的方式来控制异步调用的并发。
定义线程池
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
//开启异步
@EnableAsync
//配置类
@Configuration
class TaskPoolConfig {
//bean名称
@Bean("taskExecutor")
public Executor taskExecutor() {
//ThreadPool Task Executor 线程池 任务 执行器
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数
executor.setCorePoolSize(10);
//最大线程数
executor.setMaxPoolSize(20);
//缓冲队列200
executor.setQueueCapacity(200);
//允许线程的空闲时间60秒
executor.setKeepAliveSeconds(60);
//线程池名的前缀
executor.setThreadNamePrefix("taskExecutor-");
//线程池对拒绝任务的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
}
capacity
英 /kəˈpæsəti/ 美 /kəˈpæsəti/ 全球(英国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n. 能力,才能;容积,容纳能力;职位,职责;功率,容积;生产量,生产能力
adj. 无虚席的,满场的
设置了以下这些参数:
-
核心线程数10:线程池创建时候 初始化的线程数
-
最大线程数20:线程池最大的线程数,
- 只有在缓冲队列 满了之后 才会申请 超过核心线程数 的线程
-
缓冲队列200:用来缓冲执行任务的队列
-
允许线程的空闲时间60秒:
- 当超过了核心线程出之外的线程 在空闲时间到达之后 会被销毁
-
线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
-
线程池对拒绝任务的处理策略:这里采用了
CallerRunsPolicy
策略,- 当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中 运行被拒绝的任务;
- 如果执行程序已关闭,则会丢弃该任务
-
核心10个。最大20。缓冲队列200。有毛病,最大应该设置特别大。
-
即:超过10个后,还可在 缓冲200个,但不能超过最大线程。
使用线程池
只需要在@Async
注解中指定线程池名即可,比如:
@Slf4j
@Component
public class Task {
public static Random random = new Random();
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
}
}
单元测试重要
//@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
//测试,就是主方法执行完毕,就销毁
@Test
public void test() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
//不和action一样(action的话,后台会都执行完),如果不做拦截。主线程执行完毕,就结束了。
//直接拦截,卡死当前线程。里面加参数,如5000,只会卡死5秒。
Thread.currentThread().join();
}
}
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
//@RunWith(SpringJUnit4ClassRunner.class) 注释了更好。
thread.Join
-
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为 顺序执行的线程。
-
比如在线程B 中调用了 线程A的 Join()方法,
- 直到线程A执行完毕后,才会继续执行线程B。
-
t.join(); //调用join方法,等待线程t执行完毕
-
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
4. ThreadPoolTaskScheduler线程池的优雅关闭
改造 task
改造之前的异步任务,让它依赖一个外部资源,比如:Redis
@Slf4j
@Component
public class Task {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("开始做任务一");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
}
}
改造 test方法,模拟高并发
注意:这里省略了pom.xml中引入依赖和配置redis的步骤
模拟高并发情况下ShutDown的情况:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
@Test
@SneakyThrows
public void test() {
for (int i = 0; i < 10000; i++) {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
if (i == 9999) {
System.exit(0);
}
}
}
}
说明:通过for循环往上面定义的线程池中提交任务,
由于是异步执行,在执行过程中,利用System.exit(0)
来关闭程序,此时由于有任务在执行,就可以观察这些异步任务的销毁
- 与Spring容器中其他资源的顺序是否安全。
第四步:运行上面的单元测试,我们将碰到下面的异常内容。
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at redis.clients.util.Pool.getResource(Pool.java:53) ~[jedis-2.9.0.jar:na]
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226) ~[jedis-2.9.0.jar:na]
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16) ~[jedis-2.9.0.jar:na]
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
... 19 common frames omitted
Caused by: java.lang.InterruptedException: null
如何解决
原因分析
从异常信息JedisConnectionException: Could not get a resource from the pool
来看,我们很容易的可以想到,
- 在应用关闭的时候异步任务还在执行,由于Redis连接池先销毁了,
- 导致异步任务中要访问Redis的操作就报了上面的错。
- 所以,我们得出结论,上面的实现方式在应用关闭的时候是不优雅的,那么我们要怎么做呢?
解决方法
要解决上面的问题很简单,Spring的ThreadPoolTaskScheduler
为我们提供了相关的配置,只需要加入如下设置即可:
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("taskExecutor-");
//等待 任务 完成后 在关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待 超过这个时间,就自动销毁
executor.setAwaitTerminationSeconds(60);
return executor;
}
线程池关闭的时候 等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。
同时,这里还设置了setAwaitTerminationSeconds(60)
,
- 该方法用来设置线程池中任务的等待时间,
- 如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
termination
英 /ˌtɜːmɪˈneɪʃn/ 美 /ˌtɜːrmɪˈneɪʃn/ 全球(英国)
简明 牛津 新牛津 韦氏 例句 百科
n. 终止妊娠,人工流产;结束,终止;<美>解聘,解雇;<美>暗杀;词尾(尤指屈折变化或派生词的词尾);<古>结局
terminate
英 /ˈtɜːmɪneɪt/ 美 /ˈtɜːrmɪneɪt/ 全球(美国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
v. (使)结束,(使)终止;到达终点站;终止妊娠,人工流产;<美>解雇;<美>谋杀(某人);在……结尾,以……收尾
adj. 结束的
termina
英 美 全球(罗马尼亚)
简明 例句 百科
n. 接线柱;终端
网络释义
完成
目的
term
英 /tɜːm/ 美 /tɜːrm/ 全球(美国)
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n. (某人做某事或某事发生的)时期,期限,任期;学期,开庭期;到期,期满;术语,专有名词;措辞,说话的方式(terms);方面,角度(terms);(人们之间的)关系(terms);条件,条款(terms);付款条件,购买(出售)条件(terms);(结束战争,解决争端的)条件(terms);(数学运算中的)项;(逻)(三段论中的)项;(尤指苏格兰的)法定结账日(term day);<法律> 有限期租用的地产;(建筑)界标
v. 把……称为,把……叫做
5. 使用Future以及定义超时
-
关于返回
Future
的使用方法 -
以及对异步执行的超时控制,
定义异步任务
改造主类:主类等子类执行完毕
@Slf4j
@Component
public class Task {
public static Random random = new Random();
@Async("taskExecutor")
public Future<String> run() throws Exception {
long sleep = random.nextInt(10000);
log.info("开始任务,需耗时:" + sleep + "毫秒");
Thread.sleep(sleep);
log.info("完成任务");
return new AsyncResult<>("test");
}
}
什么是Future
类型?
Future
是对于具体的
Runnable
或者Callable
任务的执行结果- 进行取消、查询是否完成、获取结果的接口。
- 必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
它的接口定义如下:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
它声明这样的五个方法:
- cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。
- 参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。
- 如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;
- 如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
- isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
- isDone方法表示任务是否已经完成,若任务完成,则返回true;
- get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
- get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
也就是说Future提供了三种功能:
- 判断任务是否完成;
- 能够中断任务;
- 能够获取任务执行结果。
测试执行与定义超时
Future<String> futureResult = task.run();
//futureResult.get();主类等子类执行完毕
String result = futureResult.get(5, TimeUnit.SECONDS);
log.info(result);
我们在get方法中还定义了该线程执行的超时时间,通过执行这个测试我们可以观察到执行时间超过5秒的时候,这里会抛出超时异常,该执行线程就能够因执行超时而释放回线程池,不至于一直阻塞而占用资源。