详解Spring/Spring boot异步任务编程WebAsyncTask

今天一起学习下如何在Spring中进行异步编程。我们都知道,web服务器处理请求 request 的线程是从线程池中获取的,这也不难解释,因为当web请求并发数非常大时,如何一个请求进来就创建一条处理线程,由于创建线程和线程上下文切换的开销是比较大的,web服务器最终将面临崩溃。另外,web服务器创建的处理线程从头到尾默认是同步执行的,也就是说,假如处理线程A负责处理请求B,那么当B没有 return 之前,处理线程A是不可以脱身去处理别的请求的,这将极大限制了web服务器的并发处理能力。

因此线程池解决了线程可循环利用的问题,那同步处理请求怎么去解决呢?答案是异步处理。什么是异步处理呢?异步处理主要是让上面的B请求处理完成之前,能够将A线程空闲出来继续去处理别的请求。那么我们可以这样做,在A线程内部重新开启一个线程C去执行任务,让A直接返回给web服务器,继续接受新进来的请求。

在开始下面的讲解之前,我在这里先区别下两个概念:

1、处理线程

处理线程属于web服务器,负责处理用户请求,采用线程池管理

2、异步线程

异步线程属于用户自定义的线程,可采用线程池管理

spring中提供了对异步任务的支持,采用 WebAsyncTask 类即可实现异步任务,同时我们也可以对异步任务设置相应的回调处理,如当任务超时、抛出异常怎么处理等。异步任务通常非常实用,比如我们想让一个可能会处理很长时间的操作交给异步线程去处理,又或者当一笔订单支付完成之后,开启异步任务查询订单的支付结果。

一、正常异步任务

为了演示方便,异步任务的执行采用 Thread.sleep(long) 模拟,现在假设用户请求以下接口 :

http://localhost:7000/demo/getUserWithNoThing.json

异步任务接口定义如下:

/**
 * 测试没有发生任何异常的异步任务
 */
@RequestMapping(value = "getUserWithNoThing.json", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithNoThing() {
 // 打印处理线程名
 System.err.println("The main Thread name is " + Thread.currentThread().getName());

 // 此处模拟开启一个异步任务,超时时间为10s
 WebAsyncTask<String> task1 = new WebAsyncTask<String>(10 * 1000L, () -> {
 	System.err.println("The first Thread name is " + Thread.currentThread().getName());
 	// 任务处理时间5s,不超时
 	Thread.sleep(5 * 1000L);
 	return "任务1顺利执行成功!任何异常都没有抛出!";
 });

 // 任务执行完成时调用该方法
 task1.onCompletion(() -> {
 	System.err.println("任务1执行完成啦!");
 });

 System.err.println("task1继续处理其他事情!");
 return task1;
}

控制台打印如下:

The main Thread name is http-nio-7000-exec-1
task1继续处理其他事情!
The first Thread name is MvcAsync1
任务1执行完成啦!

浏览器结果如下:

二、抛异常异步任务

接口调用 : http://localhost:7000/demo/getUserWithError.json

/**
 * 测试发生error的异步任务
 * @return
 */
@RequestMapping(value = "getUserWithError.json", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithError() {
	System.err.println("The main Thread name is " + Thread.currentThread().getName());

	// 此处模拟开启一个异步任务
	WebAsyncTask<String> task3 = new WebAsyncTask<String>(10 * 1000L, () -> {
		System.err.println("The second Thread name is " + Thread.currentThread().getName());
		// 此处抛出异常
		int num = 9 / 0;
		System.err.println(num);
		return "";
	});

	// 发生异常时调用该方法
	task3.onError(() -> {
		System.err.println("====================================" + Thread.currentThread().getName()
				+ "==============================");
		System.err.println("任务3发生error啦!");
		return "";
	});
	// 任务执行完成时调用该方法
	task3.onCompletion(() -> {
		System.err.println("任务3执行完成啦!");
	});

	System.err.println("task3继续处理其他事情!");
	return task3;
}

控制台输出如下:

The main Thread name is http-nio-7000-exec-1
task3继续处理其他事情!
The second Thread name is MvcAsync1
2018-06-15 09:40:13.538 ERROR 9168 --- [nio-7000-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception

java.lang.ArithmeticException: / by zero
at com.example.demo.controller.GetUserInfoController.lambda$5(GetUserInfoController.java:93) ~[classes/:na]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:317) ~[spring-web-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_161]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_161]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_161]

2018-06-15 09:40:13.539 ERROR 9168 --- [nio-7000-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/demo] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero
at com.example.demo.controller.GetUserInfoController.lambda$5(GetUserInfoController.java:93) ~[classes/:na]
at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:317) ~[spring-web-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_161]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_161]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_161]

====================================http-nio-7000-exec-2==============================
任务3发生error啦!
任务3执行完成啦!

当然你也可以对上面做一些异常处理,不至于在用户看来显得不友好,关于异常处理,可以查看我的另一篇文章 Spring boot/Spring 统一错误处理方案的使用

浏览器输出结果:

三、超时异步任务

接口调用 : http://localhost:7000/demo/getUserWithTimeOut.json

/**
 * 测试发生任务超时的异步任务
 * @return
 */
@RequestMapping(value = "getUserWithTimeOut.json", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithTimeOut() {
 System.err.println("The main Thread name is " + Thread.currentThread().getName());

 // 此处模拟开启一个异步任务,超时10s
 WebAsyncTask<String> task2 = new WebAsyncTask<String>(10 * 1000L, () -> {
 	System.err.println("The second Thread name is " + Thread.currentThread().getName());
 	Thread.sleep(20 * 1000L);
 	return "任务2执行超时!";
 });

 // 任务超时调用该方法
 task2.onTimeout(() -> {
 	System.err.println("====================================" + Thread.currentThread().getName()
 			+ "==============================");
 	return "任务2发生超时啦!";
 });

 // 任务执行完成时调用该方法
 task2.onCompletion(() -> {
 	System.err.println("任务2执行完成啦!");
 });

 System.err.println("task2继续处理其他事情!");
 return task2;
}

控制台执行结果:

The main Thread name is http-nio-7000-exec-4
task2继续处理其他事情!
The second Thread name is MvcAsync2
====================================http-nio-7000-exec-5==============================
任务2执行完成啦!

浏览器执行结果:

四、线程池异步任务

上面的三种情况中的异步任务默认不是采用线程池机制进行管理的,也就是说,一个请求进来,虽然释放了处理线程,但是系统依旧会为每个请求创建一个异步任务线程,也就是上面我们看到的 MvcAsync 开头的异步任务线程,那这样不行啊,开销特别大呀!所以我们可以采用线程池进行管理,直接在 WebAsyncTask 类构造器传入一个 ThreadPoolTaskExecutor 对象实例即可。

下面我们先看看,当对上面第一种情况执行并发请求时会出现什么情况(此处模拟对 http://localhost:7000/demo/getUserWithNoThing.json 进行并发调用):

控制台输出如下:

The first Thread name is MvcAsync57
The first Thread name is MvcAsync58
The first Thread name is MvcAsync59
The first Thread name is MvcAsync60
The first Thread name is MvcAsync61
The first Thread name is MvcAsync62
The first Thread name is MvcAsync63
The first Thread name is MvcAsync64
The first Thread name is MvcAsync65
The first Thread name is MvcAsync66
The first Thread name is MvcAsync67
The first Thread name is MvcAsync68
The first Thread name is MvcAsync69
The first Thread name is MvcAsync70
The first Thread name is MvcAsync71
The first Thread name is MvcAsync72
The first Thread name is MvcAsync73
The first Thread name is MvcAsync74
The first Thread name is MvcAsync76
The first Thread name is MvcAsync75
The first Thread name is MvcAsync77
The first Thread name is MvcAsync78
The first Thread name is MvcAsync79
The first Thread name is MvcAsync80

由于没有加入线程池,所以100个请求将开启100个异步任务线程,开销特别大,不推荐。

下面是采用线程池的实现 :

调用接口 : http://localhost:7000/demo/getUserWithExecutor.json

/**
 * 测试线程池
 * @return
 */
@RequestMapping(value = "getUserWithExecutor.json", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithExecutor() {
 System.err.println("The main Thread name is " + Thread.currentThread().getName());

 // 此处模拟开启一个异步任务,此处传入一个线程池
 WebAsyncTask<String> task1 = new WebAsyncTask<String>(10 * 1000L, executor, () -> {
 	System.err.println("The first Thread name is " + Thread.currentThread().getName());
 	Thread.sleep(5000L);
 	return "任务4顺利执行成功!任何异常都没有抛出!";
 });

 // 任务执行完成时调用该方法
 task1.onCompletion(() -> {
 	System.err.println("任务4执行完成啦!");
 });

 System.err.println("task4继续处理其他事情!");
 return task1;
}

线程池定义如下:

@Configuration
public class MyExecutor {

 @Bean
 public static ThreadPoolTaskExecutor getExecutor() {
 	ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
 	taskExecutor.setCorePoolSize(30);
 	taskExecutor.setMaxPoolSize(30);
 	taskExecutor.setQueueCapacity(50);
 	taskExecutor.setThreadNamePrefix("huang");// 异步任务线程名以 huang 为前缀
 	return taskExecutor;
 }
}

对上面进行并发测试,可以得出下面结果 :

本文示例代码地址: https://github.com/SmallerCoder/WebAsyncTask

采用线程池可以节约服务器资源,优化服务器处理能力,要记得常用哟!谢谢阅读!觉得对你有帮助,请给个start哦!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Spring Boot利用@Async异步调用:使用Future及定义超时详解

    前言 之前连续写了几篇关于使用@Async实现异步调用的内容,也得到不少童鞋的反馈,其中问题比较多的就是关于返回Future的使用方法以及对异步执行的超时控制,所以这篇就来一起讲讲这两个问题的处理. 如果您对于@Async注解的使用还不了解的话,可以看看之前的文章,具体如下: 使用@Async实现异步调用 使用@Async实现异步调用:自定义线程池 使用@Async实现异步调用:资源优雅关闭 定义异步任务 首先,我们先使用@Async注解来定义一个异步任务,这个方法返回Future类型,具体如下

  • Spring Boot利用@Async如何实现异步调用:自定义线程池

    前言 在之前的Spring Boot基础教程系列中,已经通过<Spring Boot中使用@Async实现异步调用>一文介绍过如何使用@Async注解来实现异步调用了.但是,对于这些异步执行的控制是我们保障自身应用健康的基本技能.本文我们就来学习一下,如果通过自定义线程池的方式来控制异步调用的并发. 本文中的例子我们可以在之前的例子基础上修改,也可以创建一个全新的Spring Boot项目来尝试. 定义线程池 第一步,先在Spring Boot主类中定义一个线程池,比如: @SpringBoo

  • spring boot中使用@Async实现异步调用任务

    什么是"异步调用"? "异步调用"对应的是"同步调用",同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行:异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序.  同步调用 下面通过一个简单示例来直观的理解什么是同步调用: 定义Task类,创建三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10秒内) package com.kfit.task; import java.uti

  • Spring Boot利用@Async异步调用:ThreadPoolTaskScheduler线程池的优雅关闭详解

    前言 之前分享了一篇关于Spring Boot中使用@Async来实现异步任务和线程池控制的文章:<Spring Boot使用@Async实现异步调用:自定义线程池>.由于最近身边也发现了不少异步任务没有正确处理而导致的不少问题,所以在本文就接前面内容,继续说说线程池的优雅关闭,主要针对ThreadPoolTaskScheduler线程池. 问题现象 在上篇文章的例子Chapter4-1-3中,我们定义了一个线程池,然后利用@Async注解写了3个任务,并指定了这些任务执行使用的线程池.在上文

  • Spring Boot异步调用@Async过程详解

    在实际开发中,有时候为了及时处理请求和进行响应,我们可能会多任务同时执行,或者先处理主任务,也就是异步调用,异步调用的实现有很多,例如多线程.定时任务.消息队列等, 我们来讲讲@Async异步方法调用. 一.@Async使用演示 @Async是Spring内置注解,用来处理异步任务,在SpringBoot中同样适用,且在SpringBoot项目中,除了boot本身的starter外,不需要额外引入依赖. 而要使用@Async,需要在启动类上加上@EnableAsync主动声明来开启异步方法. @

  • 深入理解spring boot异步调用方式@Async

    本文主要给大家介绍了关于spring boot异步调用方式@Async的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 1.使用背景 在日常开发的项目中,当访问其他人的接口较慢或者做耗时任务时,不想程序一直卡在耗时任务上,想程序能够并行执行,我们可以使用多线程来并行的处理任务,也可以使用spring提供的异步处理方式@Async. 2.异步处理方式 调用之后,不返回任何数据. 调用之后,返回数据,通过Future来获取返回数据 3.@Async不返回数据 使用@EnableAsyn

  • spring boot 使用@Async实现异步调用方法

    使用@Async实现异步调用 什么是"异步调用"与"同步调用" "同步调用"就是程序按照一定的顺序依次执行,,每一行程序代码必须等上一行代码执行完毕才能执行:"异步调用"则是只要上一行代码执行,无需等待结果的返回就开始执行本身任务. 通常情况下,"同步调用"执行程序所花费的时间比较多,执行效率比较差.所以,在代码本身不存在依赖关系的话,我们可以考虑通过"异步调用"的方式来并发执行. &q

  • 详解Spring/Spring boot异步任务编程WebAsyncTask

    今天一起学习下如何在Spring中进行异步编程.我们都知道,web服务器处理请求 request 的线程是从线程池中获取的,这也不难解释,因为当web请求并发数非常大时,如何一个请求进来就创建一条处理线程,由于创建线程和线程上下文切换的开销是比较大的,web服务器最终将面临崩溃.另外,web服务器创建的处理线程从头到尾默认是同步执行的,也就是说,假如处理线程A负责处理请求B,那么当B没有 return 之前,处理线程A是不可以脱身去处理别的请求的,这将极大限制了web服务器的并发处理能力. 因此

  • SpringBoot详解整合Spring Boot Admin实现监控功能

    目录 监控 监控的意义 可视化监控平台 监控原理 自定义监控指标 监控 ​ 在说监控之前,需要回顾一下软件业的发展史.最早的软件完成一些非常简单的功能,代码不多,错误也少.随着软件功能的逐步完善,软件的功能变得越来越复杂,功能不能得到有效的保障,这个阶段出现了针对软件功能的检测,也就是软件测试.伴随着计算机操作系统的逐步升级,软件的运行状态也变得开始让人捉摸不透,出现了不稳定的状况.伴随着计算机网络的发展,程序也从单机状态切换成基于计算机网络的程序,应用于网络的程序开始出现,由于网络的不稳定性,

  • 详解在Spring MVC或Spring Boot中使用Filter打印请求参数问题

    使用Spring MVC或Spring Boot中打印或记录日志一般使用AOP记录Request请求和Response响应参数,在不使用AOP的前提下,如果在Filter中打印日志,在打印或消费请求类型为Content-Type:application/json的请求时,会出现严重的问题. 在Spring体系中,过滤器的定义我们一般采用继承OncePerRequestFilter的方式,当然也可以使用原始的Filter. 错误写法一: 如果不对request和response进行处理,使用伪代码

  • 详解用Spring Boot Admin来监控我们的微服务

    1.概述 Spring Boot Admin是一个Web应用程序,用于管理和监视Spring Boot应用程序.每个应用程序都被视为客户端,并注册到管理服务器.底层能力是由Spring Boot Actuator端点提供的. 在本文中,我们将介绍配置Spring Boot Admin服务器的步骤以及应用程序如何集成客户端. 2.管理服务器配置 由于Spring Boot Admin Server可以作为servlet或webflux应用程序运行,根据需要,选择一种并添加相应的Spring Boo

  • 详解JAVA Spring 中的事件机制

    说到事件机制,可能脑海中最先浮现的就是日常使用的各种 listener,listener去监听事件源,如果被监听的事件有变化就会通知listener,从而针对变化做相应的动作.这些listener是怎么实现的呢?说listener之前,我们先从设计模式开始讲起. 观察者模式 观察者模式一般包含以下几个对象: Subject:被观察的对象.它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify().目标类可以是接口,也可以是抽象类或具体类. ConcreteSubject:具体的

  • 详解基于Spring Data的领域事件发布

    领域事件发布是一个领域对象为了让其它对象知道自己已经处理完成某个操作时发出的一个通知,事件发布力求从代码层面让自身对象与外部对象解耦,并减少技术代码入侵. 一. 手动发布事件 // 实体定义 @Entity public class Department implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer departmentId; @Enumerate

  • 详解使用Spring的BeanPostProcessor优雅的实现工厂模式

    最近学习Spring的源码,发现一个利器BeanPostProcessor.这个后置处理器可以在bean初始化前后对bean进行操作.我们可以在初始化的时候对自己想要的bean进行缓存,进而实现自己需要处理的逻辑. 背景 当我们需要根据类型调用接口不同实现的时候,我们可以使用工厂模式实现.下面说下博主遇到过的两次需要使用工厂的场景. 场景一: 当有一个模块,我们需要根据数据库的类型实现不同的的sql.我们此时需要定义一个接口然后每一种数据库实现不同的sql.在调用时根据当前的数据库类型调用对应的

  • 详解在spring中使用JdbcTemplate操作数据库的几种方式

    使用JdbcTemplate的步骤 1.设置spring-jdbc和spring-tx的坐标(也就是导入依赖) <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency&

  • 详解Python实现多进程异步事件驱动引擎

    本文介绍了详解Python实现多进程异步事件驱动引擎,分享给大家,具体如下: 多进程异步事件驱动逻辑 逻辑 code # -*- coding: utf-8 -*- ''' author: Jimmy contact: 234390130@qq.com file: eventEngine.py time: 2017/8/25 上午10:06 description: 多进程异步事件驱动引擎 ''' __author__ = 'Jimmy' from multiprocessing import

  • 详解vue-router的Import异步加载模块问题的解决方案

    1.问题现象 2.出现问题的代码点 3.替代方案: 把import() 替换成如下: Promise.resolve().then(()=>require(`@/views/${str}`)) 4.原因分析 项目在编译时,出现一个警告 这个警告的含义: require接收了一个变量,会报上面的警告,接收一个写死的字符串值时则没有警告! 我们通过控制台查看到import()对应编译过后的代码: 从上图可以看到require接收了一个变量,所以运行时出现了警告. 那这样就会报上面找不到对应的模块.

随机推荐