提高系统性能的必备技能:异步任务完全指南

开发 前端
在传统的同步应用程序中,每个请求都需要等待处理完成后再返回结果。这种方式在处理耗时操作时会导致应用程序性能下降,响应时间增加。为了解决这个问题,异步任务应运而生。通过将耗时操作移至后台执行,异步任务可以避免阻塞主线程,提高应用程序的并发能力和响应速度。

环境:Spring5.3.23

本文将介绍Spring框架中的异步任务,阐述为什么要使用异步任务以及异步任务带来的好处。通过对Spring异步任务的深入了解,我们将掌握如何在Spring应用程序中实现高效的异步处理,并利用异步任务提高应用程序的性能和响应能力。

1. 前言

为什么要使用异步任务?

在传统的同步应用程序中,每个请求都需要等待处理完成后再返回结果。这种方式在处理耗时操作时会导致应用程序性能下降,响应时间增加。为了解决这个问题,异步任务应运而生。通过将耗时操作移至后台执行,异步任务可以避免阻塞主线程,提高应用程序的并发能力和响应速度。

异步任务的好处:

提高性能:异步任务可以避免阻塞主线程,使得应用程序能够同时处理多个请求,提高了系统的吞吐量和性能。

改善用户体验:由于异步任务无需等待耗时操作完成,因此可以快速返回结果给用户。这对于改善用户体验非常有益,用户可以在短暂的等待时间后获得响应,而无需长时间等待。

高效利用资源:异步任务可以充分利用系统资源,例如CPU和内存。在多核CPU系统中,异步任务可以同时运行多个任务,提高了资源的利用率。

降低系统负载:通过将耗时操作移至后台执行,异步任务可以减轻前台系统的负载,使其专注于处理核心业务逻辑。

适应高并发场景:在面对大量并发请求时,异步任务能够更好地应对负载压力,保证系统的稳定性和可用性。

总之,Spring异步任务为我们提供了一种高效处理耗时操作的方法,通过提高性能、改善用户体验、高效利用资源、降低系统负载以及适应高并发场景等方面的优势,帮助我们构建更加出色的应用程序。

2. 实战代码

为了演示的方便,所有示例代码我都将在一个类中完成。

在项目中要使用异步任务非常的简单,我们只需要通过一个注解开启即可,剩下的就只需要在需要异步执行的方法上添加上@Async注解即可。示例代码如下:

通过@EnableAsync开启异步任务

// 该配置类就作用就是开启异步任务的能力
@Configuration
@EnableAsync
static class AppConfig {
}

测试使用的组件类

@Component
static class AsyncService {
  
  // 我们只需要在我们的方法上添加@Async即可
  // 这样该方法的执行将会在另外的线程中执行
  @Async
  public void calc() {
    System.out.printf("执行线程: %s - 开始执行%n", Thread.currentThread().getName()) ;
    try {
      // 模拟耗时的操作
      TimeUnit.SECONDS.sleep(2) ;
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.printf("线程: %s - 执行完成%n", Thread.currentThread().getName()) ;
  }
}

测试代码

try (GenericApplicationContext context = new GenericApplicationContext()) {
  // 容器中注册相关的Bean
  context.registerBean(ConfigurationClassPostProcessor.class) ;
  context.registerBean(AppConfig.class) ;
  context.registerBean(AsyncService.class) ;
  context.refresh() ;
  // 从容器中获取组件
  AsyncService as = context.getBean(AsyncService.class) ;
  // 下面调用3次任务
  as.calc() ; 
  as.calc() ;
  as.calc() ;
  System.out.println("主线程结束...") ;
  System.in.read() ;
}

执行结果

主线程结束...
执行线程: SimpleAsyncTaskExecutor-1 - 开始执行
执行线程: SimpleAsyncTaskExecutor-2 - 开始执行
执行线程: SimpleAsyncTaskExecutor-3 - 开始执行
线程: SimpleAsyncTaskExecutor-2 - 执行完成
线程: SimpleAsyncTaskExecutor-1 - 执行完成
线程: SimpleAsyncTaskExecutor-3 - 执行完成

主线程早早的执行完了,每次方法的调用都在不同的线程,与阻塞执行相比大大提高了系统的吞吐量。

使用就是这么简单,但是我们还需要更加的深入了解这里异步执行的线程是什么样的一个线程池?是否可以自定义自己的线程池?接下来就从这2个问题来更加的深入学习异步任务执行的原理。

3. 异步任务使用的线程池

在Spring中使用异步任务的底层原理主要是通过Spring AOP(面向切面编程)来实现的。AOP是一种编程思想,它通过在程序执行的关键点上添加横切关注点,来提高代码的复用性和可维护性。

在Spring异步任务中,AOP被用于拦截方法的执行,将耗时的任务放入线程池中执行,从而避免阻塞主线程。具体来说,Spring异步任务底层使用了Java的Future和Callable接口,以及线程池技术来实现异步执行。

首先,当我们在Spring中定义一个异步方法时,实际上该方法并不会立即执行,而是会被封装为一个Callable对象。Callable接口与Runnable接口类似,但它可以返回结果,并可以抛出异常。

异步任务执行的核心处理器类是:AsyncAnnotationBeanPostProcessor

该处理器的创建是在@EnableAsync注解中的@Import导入的类

public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
  @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
    AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
    // 线程池是引用的父类中的成员
    bpp.configure(this.executor, this.exceptionHandler);
    return bpp;
  }
}
// 父类AbstractAsyncConfiguration 
public abstract class AbstractAsyncConfiguration implements ImportAware {
  protected Supplier<Executor> executor;
  // 这里的入参是我们可以自定义实现的地方,后面会讲到
  @Autowired
  void setConfigurers(ObjectProvider<AsyncConfigurer> configurers) {
    Supplier<AsyncConfigurer> configurer = SingletonSupplier.of(() -> {
      List<AsyncConfigurer> candidates = configurers.stream().collect(Collectors.toList());
      if (CollectionUtils.isEmpty(candidates)) {
        return null;
      }
      // 如果系统中定义了多个AsyncConfigurer将会抛出异常
      if (candidates.size() > 1) {
        throw new IllegalStateException("Only one AsyncConfigurer may exist");
      }
      return candidates.get(0);
    });
    // 如果没有自定义,则调用AsyncConfigurer#getAsyncExecutor,默认这个方法返回的是null
    // 所以,在默认情况下,这里的executor还是为null
    this.executor = adapt(configurer, AsyncConfigurer::getAsyncExecutor);
    this.exceptionHandler = adapt(configurer, AsyncConfigurer::getAsyncUncaughtExceptionHandler);
  }
}

接着进入核心的处理器类AsyncAnnotationBeanPostProcessor 该类中现在设置的executor还是为null。

public class AsyncAnnotationBeanPostProcessor {
  // 在示例化当前处理器过程中会执行setBeanFactory方法
  // 该方法中会定义AOP的切面(低级切面)Advisor
  public void setBeanFactory(BeanFactory beanFactory) {
    super.setBeanFactory(beanFactory);
    // 该构造方法中会构建相应的通知及切入点
    AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
  }
}
// 切面
public class AsyncAnnotationAdvisor {
  public AsyncAnnotationAdvisor(...) {
    // 构建通知拦截器
    this.advice = buildAdvice(executor, exceptionHandler);
    this.pointcut = buildPointcut(asyncAnnotationTypes);
  }
  protected Advice buildAdvice() {
    // 该拦截器说下继承关系
    // 1. AnnotationAsyncExecutionInterceptor继承 AsyncExecutionInterceptor
    // 2. AsyncExecutionInterceptor 继承 AsyncExecutionAspectSupport
    AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);
    // 在该方法中进行初始化线程池
    // 调用父类AsyncExecutionAspectSupport#configure方法
    interceptor.configure(executor, exceptionHandler);
    return interceptor; 
  }
}
public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport {
  protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
    // 先调用父类,默认情况下父类返回null,下面有分析
    Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
    // 当为null,这里就创建默认的线程池SimpleAsyncTaskExecutor
    // 这也就是上面的示例代码中默认线程池名称打印的是SimpleAsyncTaskExecutor-*
    return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
  }
}
public abstract class AsyncExecutionAspectSupport {
  public void configure(@Nullable Supplier<Executor> defaultExecutor,
      @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
    // defaultExecutor为null,则会获取系统默认的getDefaultExecutor
    // getDefaultExecutor这里的方法被子类AsyncExecutionInterceptor重写了
    this.defaultExecutor = new SingletonSupplier<>(defaultExecutor, () -> getDefaultExecutor(this.beanFactory));
  }
  // 初始化系统默认的线程池
  protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
    if (beanFactory != null) {
      try {
        // 从容器中查找TaskExcutor类型的Bean
        return beanFactory.getBean(TaskExecutor.class);
      } catch (NoUniqueBeanDefinitionException ex) {
        try {
          // 如果容器中有多个这种Bean,则在通过beanName获取
          // beanName = taskExecutor
          return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
        }
      } catch (NoSuchBeanDefinitionException ex) {
        try {
          // 如果指定beanName=taskExecutor类型为TaskExecutor的Bean
          // 则在获取beanName=taskExecutor类型为Executor类型的Bean
          return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
        }
      }
    }
    return null;
  }
}

分析到这,在我们当前的环境下是没有TaskExecutor或Executor类型的Bean。所以程序这里最终返回还是null。那这个默认线程池是谁呢?继续向下看

在上面的buildAdvice方法中构建拦截器AnnotationAsyncExecutionInterceptor该拦截器是执行的核心

public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor {
  public Object invoke(final MethodInvocation invocation) throws Throwable {
    // 确定任务执行的线程池
    AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
  }
}

到此分析完了Spring的异步任务执行使用线程池的情况。现总结下查找线程池的流程步骤:

  • 容器中查找AsyncConfigurer
  • 在1中没有,则容器中查找TaskExecutor类型的Bean,如果正好有一个则使用,如果有多个则从容器中查找beanName=taskExecutor,类型为Executor,如果没有则返回null。
  • 在2中如果没有TaskExecutor类型的Bean,则从容器中查找beanName=taskExecutor,类型为Executor,如果没有则返回null。
  • 到此都还是没有,则直接创建SimpleAsyncTaskExecutor对象作为线程池。

4. 自定义线程池

通过上面的分析你应该知道了如何自定义线程池了。

自定义AsyncConfigurer

@Component
static class CustomAsyncConfigurer implements AsyncConfigurer {


  @Override
  public Executor getAsyncExecutor() {
    return new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadFactory() {
      private final AtomicInteger poolNumber = new AtomicInteger(1);
      private final ThreadGroup group = Thread.currentThread().getThreadGroup() ;
      private final AtomicInteger threadNumber = new AtomicInteger(1);
      private final String namePrefix = "pack-" + poolNumber.getAndIncrement() +"-thread-" ;
      public Thread newThread(Runnable r) {
          Thread t = new Thread(group, r,
                                namePrefix + threadNumber.getAndIncrement(),
                                0);
          if (t.isDaemon())
              t.setDaemon(false);
          if (t.getPriority() != Thread.NORM_PRIORITY)
              t.setPriority(Thread.NORM_PRIORITY);
          return t;
      }
    }) ;
  }
}

在容器中注册上面的bean后,执行结果如下:

主线程结束...
执行线程: pack-1-thread-1 - 开始执行
执行线程: pack-1-thread-2 - 开始执行
线程: pack-1-thread-2 - 执行完成
线程: pack-1-thread-1 - 执行完成
执行线程: pack-1-thread-2 - 开始执行
线程: pack-1-thread-2 - 执行完成

自定义线程池生效了。

其它方式就不尝试了。

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2011-01-05 13:48:55

Linux提高性能

2011-09-19 14:30:47

Vista缓存

2024-05-16 12:39:42

.NET异步异步编程编程

2014-08-28 09:45:30

2023-09-27 15:41:32

Linux系统

2010-04-23 15:06:41

Aix操作系统

2010-06-24 16:22:26

Linux chatt

2011-04-13 09:19:05

Oracle数据库系统性能

2018-08-10 15:04:25

2010-04-23 11:44:34

Aix系统

2009-09-29 10:39:04

Linuxlinux系统性能检测

2019-12-25 09:49:12

WebKitWindowsChrome

2010-04-30 15:53:45

Oracle系统性能

2017-08-11 19:13:01

LinuxNmon系统监控工具

2011-03-10 14:40:52

2024-11-08 14:27:52

系统设计数据库

2011-03-18 11:13:07

LAMP度量性能

2013-03-20 17:18:07

Linux系统性能调优

2020-03-02 16:25:03

性能系统软件

2010-04-09 13:26:44

点赞
收藏

51CTO技术栈公众号