Java实现异步编程

260 阅读4分钟

很早之前在项目中就应用了异步,但是一直没有系统整理,今天来整理分享一下。

异步编程允许多个事情同时发生,当程序调用需要长时间运行的方法时,它不会阻塞当前的执行流程,程序可以继续运行,使用异步编程可以大大提高我们程序的吞吐量,可以更好的面对更高的并发场景并更好的利用现有的系统资源,提升用户体验感。 所以合理在项目中运用异步编程是有必要的。

Java实现异步编程

new Thread

这是java中实现异步编程最简洁的方式,创建一个新线程

new Thread(()->{
    // 要处理的任务
}).start();

存在问题

  • 创建线程没有复用,频繁的线程创建和销毁浪费开销
  • 没有限制线程个数,可能把系统线程用尽,引发严重事故

所以这种方式只能测试时使用,不可以应用在生产环境中

线程池

Executors可以创建四种常用的线程池

  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要可灵活回收空闲线程,若无可回收,则新建线程。不设上限,提交的任务将立即执行。
  • newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool :创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor :创建一个单线程化的线程池执行任务。

创建完线程池后,我们就可以调用submit()方法向线程池里提交任务

Spring中的@Async

Spring开始提供了@Async注解用于异步方法调用,注解可以被标注在方法上,以便异步调用该方法,方法的实际会提交给Spring TaskExecutor,由指定线程池中的线程执行

先要在项目中添加@EnableAsync开启异步任务支持

  • @Async默认异步配置使用的是SimpleAsyncTaskExecutor,默认来一个任务创建一个线程,不算严格意义上的线程池,达不到线程复用的效果
  • 实际应用中推荐自定义@Async的线程池

自定义线程池

Spring中提供了多种线程池

  • SimpleAsyncTaskExecutor
  • SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
  • ConcurrentTaskExecutor:Executor的适配类,不推荐使用。
  • ThreadPoolTaskScheduler:支持定时任务
  • ThreadPoolTaskExecutor:本质是对java.util.concurrent.ThreadPoolExecutor的包装

配置默认线程池

只需要定义一个配置类并实现AsyncConfigurer接口,重写getAsyncExecutor(),指定默认线程池

@Configuration
@EnableAsync
public class AsynConfig implements AsyncConfigurer {

    /**
     * 异步任务执行线程池
     * @return
     */
    @Bean(name = "asyncExecutor")
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setQueueCapacity(1000);
        executor.setKeepAliveSeconds(600);
        executor.setMaxPoolSize(20);
        executor.setThreadNamePrefix("taskExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    @Override
    public Executor getAsyncExecutor() {
        return asyncExecutor();
    }
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, objects) -> {
            log.error("异步任务执行出现异常, message {}, emthod {}, params {}", throwable, method, objects);
    };
}

为@Async指定线程池名字

项目中我们可以根据不同业务的特点定义不同的线程池,可以通过为@Async指定线程池名字的方式,为它指定不同的线程池,当然,如果不指定名字,用的就是默认的了

@Async("taskExecutor1")
public void task1() {
    
}
@Async("taskExecutor2")
public void task2() {
    
}

常见问题

@Async看起来很方便,但是很容易使用不当造成失效,下面是几个常见的失效场景

未使用@EnableAsync

使用@Async之前需要使用该注解开启Spring异步任务支持

内部方法调用

我们在日常开发中,经常需要在一个方法中调用另外一个方法,例如:

@Slf4j
@Service
public class UserService {
 
    public void test() {
        async("test");
    }
 
    @Async
    public void async(String value) {
        log.info("async:{}", value);
    }
}

Spring通过@Async注解实现异步的功能,底层其实是通过Spring的AOP实现的,也就是说它需要通过JDK动态代理或者cglib,生成代理对象。

向上面这样在一个类中直接调用相当于调用了this.async()方法,根本没交给Spring处理,异步功能失效

方法返回值错误

在AsyncExecutionInterceptor类的invoke()方法,会调用它的父类AsyncExecutionAspectSupport中的doSubmit方法,该方法时异步功能的核心代码,如下:

@Nullable
protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
    if (CompletableFuture.class.isAssignableFrom(returnType)) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return task.call();
            } catch (Throwable var2) {
                throw new CompletionException(var2);
            }
        }, executor);
    } else if (ListenableFuture.class.isAssignableFrom(returnType)) {
        return ((AsyncListenableTaskExecutor)executor).submitListenable(task);
    } else if (Future.class.isAssignableFrom(returnType)) {
        return executor.submit(task);
    } else {
        executor.submit(task);
        return null;
    }
}

可以看出方法的返回值要么是null,要么是Future,因此在实际项目中,要想使用@Async相关方法返回值必须是void或者Future

方法用static修饰

因为这种情况idea会直接报错:Methods annotated with '@Async' must be overridable

使用@Async注解声明的方法,必须是能被重写的。

参考文章@Async注解失效的 9 种场景