意外的异步行为:Springs的@Async vs RxJava

时间:2015-08-29 14:45:41

标签: multithreading spring-mvc asynchronous rx-java

我正在使用Spring,RxJava和非阻塞数据处理。在我的测试应用程序中,我想实现以下测试工作流程:

  1. [RT]接收请求
  2. [RT]在工作线程中异步启动处理
  3. [WT]做一些(昂贵的)初始化工作
  4. [WT]异步调用远程系统以获取值
  5. [HT]执行对远程系统的请求
  6. [HT]将响应结果转发给工作线程
  7. [WT]使用远程系统的结果做更多​​(昂贵的)工作
  8. [WT]返回最终结果
  9. RT:请求线程(Tomcat NIO)

    WT:工作线程(固定大小为1且队列大小为5的线程池)

    HT:Hystrix线程(具有默认设置的Hystrix线程池)

    (这只是模拟昂贵的数据处理与对远程资源的依赖关系的一个例子)

    我有两个代码变体:

    1. 使用@Async来调用WT(第2步)和Rx的Observable来完成剩下的工作(http://localhost:9001/value
    2. 仅使用Rx的Observables(http://localhost:9001/value-rx
    3. http://localhost:9002/value是远程资源)

      变体2工作得很好,但变体1(带@Async)遇到了一些问题。通过分析异常,线程转储,线程池状态和日志文件,看起来ListenableFuture(由步骤2中的@Async服务方法返回)无限地阻塞线程池,线程本身是等候。因此,RxJava无法在给定的线程池中根据需要运行回调代码(步骤6)。 30秒后抛出异常并且整个过程失败,因为线程池仍然被阻塞,我不明白为什么。

      如果我多次使用变量1,则第二个(以及所有后续请求)在步骤2(而不是6)中失败,因为线程池(size = 1)仍然被ListenableFuture阻止(下面的堆栈跟踪)。

      Variant 2能够“同时”处理多个请求,直到队列已满,即使只有1个请求线程和1个工作线程。

      • 在这两种情况下,我都使用this的修改版本将Observable的实例映射到ListenableFuture
      • 我已向控制器和服务类添加了其他日志记录。这样可以更容易地查看代码部分在哪个线程中执行。

      为什么@Async造成这种情况?我该如何解决?

      以下是代码:

      App1Controller

      @Slf4j
      @RestController
      public class App1Controller {
      
          @Autowired
          private App1Service app1Service;
      
          @ResponseBody
          @RequestMapping("/value")
          public ListenableFuture<String> value() {
              final ListenableFuture<String> future;
              log.info("before invoke 'app1Service'");
              future = this.app1Service.value();
              log.info("after invoke 'app1Service'");
              return future;
          }
      
          @ResponseBody
          @RequestMapping("/value-rx")
          public ListenableFuture<String> valueRx() {
              final Observable<String> observable;
      
              log.info("before invoke 'app1Service'");
              observable = this.app1Service.valueRx();
              log.info("after invoke 'app1Service'");
      
              return new ObservableListenableFuture<>(observable);
          }
      }
      

      App1Service

      @Slf4j
      @Service
      public class App1Service {
      
          @Autowired
          private TaskExecutor taskExecutor;
      
          @Autowired
          private App2Service app2Service;
      
          @Async
          public ListenableFuture<String> value() {
              final ListenableFuture<String> future;
      
              log.info("before start processing");
              this.doSomeStuff();
              future = new ObservableListenableFuture<>(this.valueFromApp2Service());
              log.info("after start processing");
      
              return future;
          }
      
          public Observable<String> valueRx() {
              final Observable<String> observable;
      
              log.info("before start processing");
      
              observable = Observable.<String>create(s -> {
                  this.doSomeStuff();
                  this.valueFromApp2Service().subscribe(
                          result -> {
                              log.info("next (processing)");
                              s.onNext(result);
                          },
                          throwable -> {
                              log.info("error (processing)");
                              s.onError(throwable);
                          },
                          () -> {
                              log.info("completed (processing)");
                              s.onCompleted();
                          });
              }).subscribeOn(Schedulers.from(this.taskExecutor));
      
              log.info("after start processing");
      
              return observable;
          }
      
          private Observable<String> valueFromApp2Service() {
              final AsyncSubject<String> asyncSubject;
      
              log.info("before invoke 'app2Service'");
      
              asyncSubject = AsyncSubject.create();
              this.app2Service.value().observeOn(Schedulers.from(this.taskExecutor)).subscribe(
                      result -> {
                          log.info("next (from 'app2Service')");
                          asyncSubject.onNext(this.doSomeMoreStuff(result));
                      }, throwable -> {
                          log.info("error (from 'app2Service')");
                          asyncSubject.onError(throwable);
                      }, () -> {
                          log.info("completed (from 'app2Service')");
                          asyncSubject.onCompleted();
                      });
      
              log.info("after invoke 'app2Service'");
      
              return asyncSubject;
          }
      
          private void doSomeStuff() {
              log.info("do some expensive stuff");
              this.sleep(1000);
              log.info("finish some expensive stuff");
          }
      
          private String doSomeMoreStuff(final String valueFromRemote) {
              log.info("do some more expensive stuff with '{}'", valueFromRemote);
              this.sleep(2000);
              log.info("finish some more expensive stuff with '{}'", valueFromRemote);
              return "MODIFIED " + valueFromRemote;
          }
      
          private void sleep(final long milliSeconds) {
              try {
                  Thread.sleep(milliSeconds);
              } catch (final InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      

      App2Service

      @Slf4j
      @Service
      public class App2Service {
      
          @HystrixCommand(commandKey = "app2")
          public Observable<String> value() {
              Observable<String> observable;
      
              log.info("before invoke remote service");
      
              observable = new ObservableResult<String>() {
      
                  @Override
                  public String invoke() {
                      log.info("invoke");
                      return new RestTemplate().getForEntity("http://localhost:9002/value", String.class).getBody();
                  }
      
              };
      
              log.info("after invoke remote service");
      
              return observable;
          }
      }
      

      配置

      应用程序(主要/配置类):

      @EnableCircuitBreaker
      @SpringBootApplication
      public class Application {
      
          public static void main(final String[] args) {
              SpringApplication.run(Application.class, args);
          }
      
          @Configuration
          @EnableAsync
          public static class AsyncConfiguration {
      
              @Bean
              public TaskExecutor taskExecutor() {
                  final ThreadPoolTaskExecutor taskExecutor;
      
                  taskExecutor = new ThreadPoolTaskExecutor();
                  taskExecutor.setCorePoolSize(1);
                  taskExecutor.setMaxPoolSize(1);
                  taskExecutor.setQueueCapacity(5);
                  taskExecutor.setThreadNamePrefix("worker-");
      
                  return taskExecutor;
              }
          }
      }
      

      application.properties:

      server.port=9001
      server.tomcat.max-threads=1
      hystrix.command.app2.fallback.enabled=false
      hystrix.command.app2.execution.isolation.thread.timeoutInMilliseconds=15000
      

      变量1(第一次调用)的日志输出

      16:06:31.871 [nio-9001-exec-1] before invoke 'app1Service'
      16:06:31.879 [nio-9001-exec-1] after invoke 'app1Service'
      16:06:31.887 [       worker-1] before start processing
      16:06:31.888 [       worker-1] do some expensive stuff
      16:06:32.890 [       worker-1] finish some expensive stuff
      16:06:32.891 [       worker-1] before invoke 'app2Service'
      16:06:33.135 [x-App2Service-1] before invoke remote service
      16:06:33.136 [x-App2Service-1] after invoke remote service
      16:06:33.137 [x-App2Service-1] invoke
      16:06:33.167 [       worker-1] after invoke 'app2Service'
      16:06:33.172 [       worker-1] after start processing
      16:07:02.816 [nio-9001-exec-1] Exception Processing ErrorPage[errorCode=0, location=/error]
      
      java.lang.IllegalStateException: Cannot forward after response has been committed
          at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:328)
          at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:318)
          at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:439)
          at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:305)
          at org.apache.catalina.core.StandardHostValve.throwable(StandardHostValve.java:399)
          at org.apache.catalina.core.AsyncContextImpl.setErrorState(AsyncContextImpl.java:438)
          at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:291)
          at org.apache.coyote.http11.AbstractHttp11Processor.asyncDispatch(AbstractHttp11Processor.java:1709)
          at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:649)
          at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1521)
          at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1478)
          at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
          at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
          at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
          at java.lang.Thread.run(Thread.java:745)
      

      变量2的日志输出(第一次调用)

      16:07:54.465 [nio-9001-exec-1] before invoke 'app1Service'
      16:07:54.472 [nio-9001-exec-1] before start processing
      16:07:54.500 [nio-9001-exec-1] after start processing
      16:07:54.500 [nio-9001-exec-1] after invoke 'app1Service'
      16:07:54.517 [       worker-1] do some expensive stuff
      16:07:55.522 [       worker-1] finish some expensive stuff
      16:07:55.522 [       worker-1] before invoke 'app2Service'
      16:07:55.684 [x-App2Service-1] before invoke remote service
      16:07:55.685 [x-App2Service-1] after invoke remote service
      16:07:55.686 [x-App2Service-1] invoke
      16:07:55.717 [       worker-1] after invoke 'app2Service'
      16:08:05.786 [       worker-1] next (from 'app2Service')
      16:08:05.786 [       worker-1] do some more expensive stuff with 'value from app2 service'
      16:08:07.791 [       worker-1] finish some more expensive stuff with 'value from app2 service'
      16:08:07.791 [       worker-1] completed (from 'app2Service')
      16:08:07.791 [       worker-1] next (processing)
      16:08:07.792 [       worker-1] completed (processing)
      

      WT的线程转储(在使用变体1之后)

      "worker-1" #24 prio=5 os_prio=31 tid=0x00007fe2be8cf000 nid=0x5e03 waiting on condition [0x0000000123413000]
         java.lang.Thread.State: WAITING (parking)
          at sun.misc.Unsafe.park(Native Method)
          - parking to wait for  <0x00000006c0d68fb0> (a org.springframework.util.concurrent.ListenableFutureTask)
          at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
          at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
          at java.util.concurrent.FutureTask.get(FutureTask.java:191)
          at org.springframework.util.concurrent.SettableListenableFuture.get(SettableListenableFuture.java:122)
          at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:110)
          at java.util.concurrent.FutureTask.run(FutureTask.java:266)
          at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
          at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
          at java.lang.Thread.run(Thread.java:745)
      
         Locked ownable synchronizers:
          - <0x00000006c0d68170> (a java.util.concurrent.ThreadPoolExecutor$Worker)
      

      WT的线程转储(使用变体2之后)

      "worker-1" #24 prio=5 os_prio=31 tid=0x00007fc6136dd800 nid=0x5207 waiting on condition [0x000000012d638000]
         java.lang.Thread.State: WAITING (parking)
          at sun.misc.Unsafe.park(Native Method)
          - parking to wait for  <0x00000006c02f5388> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
          at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
          at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
          at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
          at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
          at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
          at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
          at java.lang.Thread.run(Thread.java:745)
      
         Locked ownable synchronizers:
          - None
      

      异步拦截器使用简单的Future,无法处理ListenableFuture。在我再次审查了线程转储后,我注意到FutureTask.get得到了。这是一个阻止电话。这意味着当使用1个线程时,变体1是内置死锁。

      此代码有效:

      控制器

      @ResponseBody
      @RequestMapping("/value")
      public ListenableFuture<String> value() {
          final SettableListenableFuture<String> future;
          this.app1Service.value(future);
          return future;
      }
      

      服务

      @Async
      public void value(final SettableListenableFuture<String> future) {
          this.doSomeStuff();
          this.valueFromApp2Service().subscribe(future::set, future::setException);
      }
      

1 个答案:

答案 0 :(得分:0)

我想你已经回答了你的问题(或者我遗漏了一些东西):

  

如果我多次使用变体1,则第二次使用变体1(以及随后的所有变量   请求),在步骤2(而不是6)中失败,因为线程   pool(size = 1)仍然被ListenableFuture阻塞(堆栈   跟踪下面)。

您的TaskExecutor只有一个可用的帖子,用于@Async。然后,从该线程,您想再次使用TaskExecutor作为Observable:

    this.app2Service.value().observeOn(Schedulers.from(this.taskExecutor)).subscribe(

但是,您的池中没有可用的线程。如果增加coreSize,或为RxJava内容定义不同的TaskExecutor ,它应该可以正常工作。

修改

如果确实需要异步执行app1Service.value(),您可以从中删除@Asynch,并将任务显式提交给taskExecutor,以便您可以向ListenableFuture。如果您将结果类型更改为DeferredResult,则可以在执行ListenableFuture的回调时设置结果:

@Autowired
private TaskExecutor taskExecutor;

@ResponseBody
@RequestMapping("/value")
public DeferredResult<String> value() {

    final DeferredResult<String> dr = new DeferredResult<String>();
    taskExecutor.execute(() -> {
        final ListenableFuture<String> future = app1Service.value();
        future.addCallback(new ListenableFutureCallback<String>() {

            @Override
            public void onSuccess(String result) {
                dr.setResult(result);
            }

            @Override
            public void onFailure(Throwable ex) {
                dr.setErrorResult(ex);
            }
        });
    });
    return dr;
}

或者,当然,使用您的解决方案,这是更好的。