天天看点

深入解析并发编程:解析SpringAsync并发编程,Spring@Async注解

作者:程序员高级码农II

并发编程

这部分讨论Spring框架所提供的一组并发编程组件,包括任务执行器、任务调度器以及@Async注解,分析这些组件与JDK中并发编程组件之间的整合过程,并给出源码级的原理分析。通过这一部分的学习,读者将掌握如何将传统的同步执行模式转化为异步执行模式,从而提升系统的响应性和并发性。

解析Spring Async并发编程

前面两章我们讨论了Spring Boot所提供的数据访问功能及其技术组件。

在本章中,我们将关注Web开发过程中的另一个核心话题,即如何提高系统的请求响应能力。我们知道,对于Web请求而言,我们希望服务端能够快速返回处理结果,从而提高系统的响应能力。而Spring Boot框架也针对这一诉求给出了它的解决方案,就是异步编程模型。

Spring Boot对于异步编程模型的支持是多方面的。本章将首先讨论最基本的@Async注解,该注解为方法执行自动嵌入了异步线程,并提供了请求-响应以及即发-即弃这两种处理模式。另外,@Async是一种通用型的异步实现机制。而针对Web应用开发场景,Spring Boot还专门提供了WebAsyncTask工具类来简化该场景下的异步编程实现过程。

从实现原理上讲,Spring Async背后采用的是代理机制,我们已经在第3章中对这一话题有过深入的分析。而在本章中,我们将进一步从@Async注解入手并逐步剖析它的原理,从而加深你对代理机制的核心概念以及具体应用场景的理解和把握。

Spring @Async注解

异步处理的主要优势是调用方不必等待被调用方完成执行过程。

为了在独立的线程中执行方法,Spring异步编程模型提供了一个全新的@Async注解。

该注解可以与JDK中的Future机制以及线程池进行无缝整合。

@Async注解的异步处理机制

为了更好地理解@Async注解所具备的异步处理机制,我们先从一个简单的示例开始说起。假设我们存在一个如代码清单10-1所示的SyncTask类。简单起见,这里我们对一些变量定义和工具方法的实现过程进行了省略。

代码清单10-1 SyncTask类代码

public class SyncTask {

...

public void operation1() throws Exception {

System.out.println("Operation1 Started");

long startTime = currentTimeMillis();

sleep(random.nextInt(10000));

long endTime = currentTimeMillis();

System.out.println("Operation1 Finished:" + (endTime -

startTime));

}

public void operation2() throws Exception {

System.out.println("Operation2 Started");

long startTime = currentTimeMillis();

sleep(random.nextInt(10000));

long endTime = currentTimeMillis();

System.out.println("Operation2 Finished:" + (endTime -

startTime)); }

}

现在,让我们创建一个SyncTask类的实例,并依次执行它的operation1()和operation2()方法,结果如代码清单10-2所示。

代码清单10-2 SyncTask类执行日志

Operation1 Started

Operation1 Finished:4059

Operation2 Started

Operation2 Finished:6316

以上结果是完全可以预见的。对于operation1()和operation2()方法而言,它们将按照被调用的顺序依次生成日志。

现在,让我们对SyncTask类进行重构,并实现如代码清单10-3所示的AsyncTask类。

代码清单10-3 AsyncTask类代码

public class AsyncTask {

...

@Async

public void operation1() throws Exception {

System.out.println("Operation1 Started");

long startTime = currentTimeMillis();

sleep(random.nextInt(10000));

long endTime = currentTimeMillis();

System.out.println("Operation1 Finished:" + (endTime -

startTime));

}

@Async public void operation2() throws Exception {

System.out.println("Operation2 Started");

long startTime = currentTimeMillis();

sleep(random.nextInt(10000));

long endTime = currentTimeMillis();

System.out.println("Operation2 Finished:" + (endTime -

startTime));

}

}

可以看到,AsyncTask类和SyncTask类的唯一区别是前者在operation1()和operation2()方法上分别添加了@Async注解。这时候,如果我们再次执行这两个方法,那么得到的结果就不一定是operation1()的Operation1Started日志出现在operation2()的Operation2 Started日志之前了,而可能相反。事实上,在日志中甚至可能不会出现任何输出。

究其原因,在于AsyncTask类中的operation1()和operation2()方法已经在独立的线程中异步执行了。通过@Async注解,主线程在触发异步执行线程之后,并不会理会operation1()和operation2()方法所对应的线程是否已经执行完毕。

由于没有其他需要执行的内容,主线程就自动结束了,导致可能出现日志不完整甚至是没有任何相关输出的情况。

@Async注解的两种处理模式

想要在Spring Boot应用程序中启用异步编程模式,我们可以通过@EnableAsync注解来实现。常见的做法是在专门的配置类上添加这一注解,如代码清单10-4所示。

代码清单10-4 @EnableAsync注解使用示例代码

@Configuration

@EnableAsyncpublic class SpringConfig { ... }

@Async注解支持两种处理模式,即普通的请求-响应模式以及前面介绍的即发-即弃模式。

我们先来看即发-即弃模式的代码示例,如代码清单10-5所示。

代码清单10-5 基于@Async注解的即发-即弃模式使用示例代码

@Async

public void recordUserHealthData() {

logger.info("Record user health data successfully.");

}

可以看到,我们在一个返回值为void的方法上添加了@Async注解,这样该方法中的方法体代码将以异步的方式进行执行。

然后,我们来看一下请求-响应式的异步执行模式的代码示例,如代码清单10-6所示。

代码清单10-6 基于@Async注解的请求-响应模式示例代码

@Service

public class HealthService {

@Async

public Future<String> getHealthDescription() throws

InterruptedException {

LOGGER.info("Thread id: " + Thread.currentThread().getId());

// Sleeps 2s

Thread.sleep(2000);

String healthDescription = "health description";

LOGGER.info(processInfo); return new AsyncResult<String>(healthDescription);

}

}

可以看到,这里我们在方法入口打印了当前的线程ID,然后让主线程睡眠2s来模拟长时间的业务处理流程。接着,我们返回异步调用的结果对象AsyncResult。AsyncResult是Spring框架对JDK中Future接口的一种实现,我们可以通过AsyncResult对象跟踪异步调用的结果。为了更好地理解上述方法的执行过程,我们有必要先对JDK中的Future机制展开一些讲解。

传统调用和Future模式调用的对比可以参考图10-1。

我们可以看到在Future模式调用过程中,服务调用者在向服务消费者发送请求之后,可以在没有获取返回值的情况下立即返回并继续执行其他任务,直到服务消费者通过Future机制返回调用的结果,这体现了Future调用异步化的特点。

深入解析并发编程:解析SpringAsync并发编程,Spring@Async注解

图10-1 传统调用和Future机制对比

如果我们想要从Future对象中获取结果,可以使用如代码清单10-7所示的示例代码。

代码清单10-7 获取Future对象示例代码

public void testGetHealthDescription() throws ExecutionException,

InterruptedException {

System.out.println("Current Thread:" +

Thread.currentThread().getName());

long startTime = System.currentTimeMillis();

Future<String> healthDescriptionFuture =

healthService.getHealthDescription();

//这里可以执行其他操作

healthDescriptionFuture.get();

System.out.println (System.currentTimeMillis() - startTime);

}

可以看到,我们在这里同样打印了当前执行线程,然后通过Future的get()方法获取响应结果。而在这个方法返回之前,我们可以嵌入任何其他任务。

但是请注意,在上述方法中,一旦get()方法被触发,就相当于执行了同步操作。这是因为JDK中Future的执行虽然是异步的,但是它没有提供通知机制,当前线程在获取get()方法的返回值之前也就只能等待。

本文给大家讲解的内容是并发编程:解析SpringAsync并发编程,Spring@Async注解

  • 下文给大家讲解的是并发编程:解析SpringAsync并发编程,Spring@Async实现原理

继续阅读