文章目录

  • 一、@Async
  • 1. Spring / spring boot 中启用@Async
  • 2. @Async注解使用
  • 二、使用 @Async 注意事项
  • 1. @Async默认线程池导致OOM问题
  • 2. 自定义Executor替换默认SimpleAsyncTaskExecutor
  • 执行器
  • 异常处理
  • 三、参考


一、@Async

SpringBoot中异步请求的使用
参考URL:

1. Spring / spring boot 中启用@Async

1) 基于Java配置的启用方式:
开启异步支持
@Configuration
@EnableAsync
public class SpringAsyncConfig { … }

默认情况下,@EnableAsync检测Spring的@Async注释和EJB 3.1 javax. EJB .异步;此选项还可用于检测其他用户定义的注释类型。

2)在Spring和Spring Boot中启用@Async
在启动类上添加注解:@EnableAsync

在相应的方法上添加注解:@Async

@EnableAsync
@EnableTransactionManagement
public class SettlementApplication {
    public static void main(String[] args) {
        SpringApplication.run(SettlementApplication.class, args);
    }
}

总结: spring boot环境可以使用 2) 方式即可。只要通过@Async注解就能将普通的同步任务改为异步调用任务。用@Async注解之前,我们需要在入口类添加注解@EnableAsync开启异步调用。

2. @Async注解使用

@Async
public void asyncMethodWithVoidReturnType() {
 System.out.println("Execute method asynchronously. "
 + Thread.currentThread().getName());
}
@Async
public Future<String> asyncMethodWithReturnType() {
 System.out.println("Execute method asynchronously - "
 + Thread.currentThread().getName());
 try {
 Thread.sleep(5000);
 return new AsyncResult<String>("hello world !!!!");
 } catch (InterruptedException e) {
 //
 }
 
 return null;
}

二、使用 @Async 注意事项

默认类内的方法调用不会被aop拦截,也就是说同一个类内的方法调用,@Async不生效。

总结: 被@Async标记的方法的调用者不能和被调用的方法在同一类中不然不会起作用。

1. @Async默认线程池导致OOM问题

springboot-@Async默认线程池导致OOM问题
参考URL:

内存溢出的三种类型:
1.第一种OutOfMemoryError: PermGen space,发生这种问题的原意是程序中使用了大量的jar或class
2.第二种OutOfMemoryError: Java heap space,发生这种问题的原因是java虚拟机创建的对象太多
3.第三种OutOfMemoryError:unable to create new native thread,创建线程数量太多,占用内存过大

分析:
1.初步怀疑是线程创建太多导致,使用jstack 线程号 > /tmp/oom.log将应用的线程信息打印出来。查看oom.log,发现大量线程处于Runnable状态,基本可以确认是线程创建太多了。

代码分析:
@Async默认异步配置使用的是SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个线程。

  1. SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况下,会不断创建新的线程来处理任务,核心代码如下:
public void execute(Runnable task, long startTimeout) {
   Assert.notNull(task, "Runnable must not be null");
   Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
   //判断是否开启限流机制
   if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
      //执行前置操作,进行限流
      this.concurrencyThrottle.beforeAccess();
      //执行完线程任务,会执行后置操作concurrencyThrottle.afterAccess(),配合进行限流
      doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
   }
   else {
      doExecute(taskToUse);
   }
}
  1. SimpleAsyncTaskExecutor限流实现
    首先任务进来,会循环判断当前执行线程数是否超过concurrencyLimit,如果超了,则当前线程调用wait方法,释放monitor对象锁,进入等待。
protected void beforeAccess() {
	if (this.concurrencyLimit == NO_CONCURRENCY) {
		throw new IllegalStateException(
				"Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY");
	}
	if (this.concurrencyLimit > 0) {
		boolean debug = logger.isDebugEnabled();
		synchronized (this.monitor) {
			boolean interrupted = false;
			while (this.concurrencyCount >= this.concurrencyLimit) {
				if (interrupted) {
					throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " +
							"but concurrency limit still does not allow for entering");
				}
				if (debug) {
					logger.debug("Concurrency count " + this.concurrencyCount +
							" has reached limit " + this.concurrencyLimit + " - blocking");
				}
				try {
					this.monitor.wait();
				}
				catch (InterruptedException ex) {
					// Re-interrupt current thread, to allow other threads to react.
					Thread.currentThread().interrupt();
					interrupted = true;
				}
			}
			if (debug) {
				logger.debug("Entering throttle at concurrency count " + this.concurrencyCount);
			}
			this.concurrencyCount++;
		}
	}
}

线程任务执行完毕后,当前执行线程数会减一,会调用monitor对象的notify方法,唤醒等待状态下的线程,等待状态下的线程会竞争monitor锁,竞争到,会继续执行线程任务。

解决:
开启限流机制。

@Configuration
@EnableAsync
public class AsyncCommonConfig extends AsyncConfigurerSupport {
    @Override
    public Executor getAsyncExecutor() {
        SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
        //设置允许同时执行的线程数为10
 executor.setConcurrencyLimit(10);
        return executor;
    }
}

开启限流机制总结:
1.开启限流情况下,能有效控制应用线程数
2.虽然可以有效控制线程数,但执行效率会降低,会出现主线程等待,线程竞争的情况
3.限流机制适用于任务处理比较快的场景,对于应用处理时间比较慢的场景并不适用。

最终解决办法:
1.自定义线程池,使用LinkedBlockingQueue阻塞队列来限定线程池的上限
2.定义拒绝策略,如果队列满了,则拒绝处理该任务,打印日志

public class AsyncConfig implements AsyncConfigurer{
    private Logger logger = LogManager.getLogger();

    @Value("${thread.pool.corePoolSize:10}")
    private int corePoolSize;

    @Value("${thread.pool.maxPoolSize:20}")
    private int maxPoolSize;

    @Value("${thread.pool.keepAliveSeconds:4}")
    private int keepAliveSeconds;

    @Value("${thread.pool.queueCapacity:512}")
    private int queueCapacity;

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setQueueCapacity(queueCapacity);
        executor.setRejectedExecutionHandler((Runnable r, ThreadPoolExecutor exe) -> {
                logger.warn("当前任务线程池队列已满.");
        });
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncUncaughtExceptionHandler() {
            @Override
            public void handleUncaughtException(Throwable ex , Method method , Object... params) {
                logger.error("线程池执行任务发生未知异常.", ex);
            }
        };
    }
}

2. 自定义Executor替换默认SimpleAsyncTaskExecutor

参考URL:
Springboot Async schedule
参考URL:

@Async 默认不使用线程池,使用SimpleAsyncTaskExecutor(一个线程执行器,每个任务都会新建线程去执行)。@Async 默认不限制并发线程且不重用线程。因此,为了安全起见,我们还将添加自定义任务执行器。

Spring 已经实现的异常线程池:

  1. SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
  2. SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方
  3. ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
  4. SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类
  5. ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装

思路:通过实现AsyncConfigurer自定义异常线程池,包含异常处理。实现AsyncConfigurer接口对异常线程池更加细粒度的控制。

代码如下,添加一个配置类即可:

@Slf4j
@Configuration
public class AsyncConfig implements AsyncConfigurer {

    @Value("${thread.pool.corePoolSize:10}")
    private int corePoolSize;

    @Value("${thread.pool.maxPoolSize:20}")
    private int maxPoolSize;

    @Value("${thread.pool.keepAliveSeconds:4}")
    private int keepAliveSeconds;

    @Value("${thread.pool.queueCapacity:512}")
    private int queueCapacity;

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("Async-Executor");
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setQueueCapacity(queueCapacity);
        // 设置拒绝策略
        executor.setRejectedExecutionHandler((Runnable r, ThreadPoolExecutor exe) -> {
            log.warn("当前任务线程池队列已满!");
        });
        executor.initialize();
        return executor;
    }

    /**
     * 对void方法抛出的异常处理的类AsyncUncaughtExceptionHandler
     * @return
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncUncaughtExceptionHandler() {
            @Override
            public void handleUncaughtException(Throwable ex , Method method , Object... params) {
                log.error("线程池执行任务发生未知异常.", ex);
            }
        };
    }

}

对异步方法的异常处理
在调用方法时,可能出现方法中抛出异常的情况。在异步中主要有有两种异常处理方法:

  1. 对于方法返回值是Futrue的异步方法: a) 一种是在调用future的get时捕获异常; b) 在异常方法中直接捕获异常
  2. 对于返回值是void的异步方法:通过AsyncUncaughtExceptionHandler处理异常

总结:对于返回值是Future,不会被AsyncUncaughtExceptionHandler处理,需要我们在方法中捕获异常并处理或者在调用方在调用Futrue.get时捕获异常进行处理。

生产实践中,上面这个demo已经够用。下面 执行器、 异常处理只是简单举例说明一下。

执行器

默认情况下,Spring 使用SimpleAsyncTaskExecutor去执行这些异步方法(此执行器没有限制线程数)。此默认值可以从两个层级进行覆盖

  • 方法级别
  • 应用级别
  1. 方法级别覆盖
@Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
 System.out.println("Execute method with configured executor - "
 + Thread.currentThread().getName());
}
  1. 应用级别覆盖

配置类应该实现AsyncConfigurer接口——这意味着它拥有getAsyncExecutor()方法的实现。在这里,我们将返回整个应用程序的执行器——这现在成为运行带有@Async注释的方法的默认执行器:

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
 
 @Override
 public Executor getAsyncExecutor() {
 ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
 executor.initialize();
 executor.setCorePoolSize(5);
 executor.setMaxPoolSize(10);
 executor.setQueueCapacity(25);
 return executor;
 }
 
}
异常处理

当方法返回值是Future的时候,异常捕获是没问题的 - Future.get()方法会抛出异常。

但是,如果返回类型是Void,那么异常在当前线程就捕获不到。因此,我们需要添加额外的配置来处理异常。

我们将通过实现AsyncUncaughtExceptionHandler接口创建一个定制的async异常处理程序。handleUncaughtException()方法在存在任何未捕获的异步异常时调用:

public class CustomAsyncExceptionHandler
 implements AsyncUncaughtExceptionHandler {
 
 @Override
 public void handleUncaughtException(
 Throwable throwable, Method method, Object... obj) {
 
 System.out.println("Exception message - " + throwable.getMessage());
 System.out.println("Method name - " + method.getName());
 for (Object param : obj) {
 System.out.println("Parameter value - " + param);
 }
 }
 
}

在上一步中,我们研究了由configuration类实现的AsyncConfigurer接口。作为其中的一部分,我们还需要覆盖getAsyncUncaughtExceptionHandler()方法来返回我们自定义的异步异常处理程序:

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
 return new CustomAsyncExceptionHandler();
}