玩转@Async注解异步编程

前言

异步调用几乎是处理高并发Web应用性能问题的万金油,那么什么是“异步调用”?
“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。

同步: 同步就是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果。

异步: 异步调用则是只是发送了调用的指令,调用者无需等待被调用的方法完全执行完毕;而是继续执行下面的流程。

那实现异步调用有哪些方式呢?今天我们就来说道说道!

@Async注解

对于异步方法调用,从Spring3开始提供了@Async注解,该注解可以被标注在方法上,以便异步地调用该方法。调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行。

Spring 已经实现的线程池

  1. SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。

  2. SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。

  3. ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。

  4. SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。

  5. ThreadPoolTaskExecutor :最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

@Async的默认线程池为SimpleAsyncTaskExecutor。该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。在项目应用中,@Async调用线程池,推荐使用自定义线程池的模式。自定义线程池常用方案:重新实现接口AsyncConfigurer

在Spring Boot中,我们只需要通过使用@Async注解就能简单的将原来的同步函数变为异步函数,当然前提是,这个方法所在的类必须在Spring环境中。使用@Async注解时,将注解的value指定为你Executor类型的BeanName,就可以使用指定的线程池来作为任务的载体,这样就使用线程池也更加灵活

实战案例

1.启动类添加@EnableAsync注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync  //开启异步编程
public class SpringBootConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootConfigApplication.class, args);
    }
}


2、方法上添加@Async,类上添加@Component,(确保该bean处于spring环境中)

package com.alliance.javaalliance.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;

import java.util.Random;
import java.util.concurrent.Future;


@Component
@Slf4j
public class SyncTask {
    public static Random random =new Random();

    @Async
    public Future<String> doTaskOne() throws Exception {
        String threadName = Thread.currentThread().getName();
        log.info("{}开始做任务一",threadName);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("{}完成任务一,耗时:{}毫秒",threadName,(end - start));
        return new AsyncResult<>("one done");
    }

    @Async
    public Future<String> doTaskTwo() throws Exception {
        String threadName = Thread.currentThread().getName();
        log.info("{}开始做任务二",threadName);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("{}完成任务二,耗时:{}毫秒",threadName,(end - start));
        return new AsyncResult<>("one done");
    }

    @Async
    public Future<String> doTaskThree() throws Exception {
        String threadName = Thread.currentThread().getName();
        log.info("{}开始做任务三",threadName);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("{}完成任务三,耗时:{}毫秒",threadName,(end - start));
        return new AsyncResult<>("one done");
    }

    @Async
    public Future<String> doTaskFour() throws Exception {
        String threadName = Thread.currentThread().getName();
        log.info("{}开始做任务四",threadName);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("{}完成任务四,耗时:{}毫秒",threadName,(end - start));
        return new AsyncResult<>("one done");
    }
}

为什么要写四个一样的方法,是为了后续做测试用。为什么方法的返回值是Future,是为了在测试方法中捕捉到所有方法执行结束。

3、测试方法

package com.lara.springbootconfig;

import com.lara.springbootconfig.task.SyncTask;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.Future;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SyncTaskTest {

    @Autowired
    private SyncTask syncTask;

    @Test
    public void test() throws Exception {
        long start = System.currentTimeMillis();
        Future<String> one = syncTask.doTaskOne();
        Future<String> two = syncTask.doTaskTwo();
        Future<String> three = syncTask.doTaskThree();
        Future<String> four = syncTask.doTaskFour();
        //感觉下面的while空转有点不好
        while (true) {
        //确保所有的任务都完成,if条件才算成立
            if (one.isDone() && two.isDone() && three.isDone() && four.isDone()) {
                break;
            }
        }
        long end = System.currentTimeMillis();
        log.info("全部任务已完成,耗时:{}毫秒",(end - start));
    }
}

4、测试结果

玩转@Async注解异步编程_第1张图片
可以看到四个方法启用了四个线程来执行。为什么会是这种结果呢???
这是因为在我们不指定线程池的情况下,spring默认使用SimpleAsyncTaskExecutor,它不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。当异步任务非常多时,后果不堪设想。。。

注:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效


5、使用自定义的线程池

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class ThreadPoolConfig {
//上述线程池的参数含义为什么设置为2,3,4,没有为什么仅仅是为了测试使用
    @Bean
    public TaskExecutor taskExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);//核心线程数
        executor.setMaxPoolSize(3);//最大线程数
        executor.setKeepAliveSeconds(60);//多余线程存活时间
        executor.setQueueCapacity(4);//队列容量
        //指定线程名称前缀
        executor.setThreadNamePrefix("taskExecutor");
        return executor;
    }
}

加了上述配置后,还要把用到@Async的地方后面指定线程池名称,即@Async("taskExecutor"),然后再次运行相同的测试代码,看看结果。
玩转@Async注解异步编程_第2张图片
可以看到我们自定义线程池生效了,但是只有两个线程在执行任务。为什么呢?因为我们设置的核心线程是2个,前2个任务到达时,创新新的线程执行。第3个任务到达后,放进任务队列中。第4个任务到达后也放进任务队列。

你可能感兴趣的:(JavaWeb基础,java,后端,分布式,架构)