java实现异步非阻塞的几种方式-同步阻塞调用

1. 同步阻塞调用

在讲异步非阻塞之前还是先来说明同步阻塞的调用吧。明白了同步阻塞的调用,才能更好的明白异步非阻塞的调用。以一个示例来说明吧,这是一个非常常见的程序间的调用。

我们的程序对外提供当前的用户的订单详细查询的接口,订单接口先调用用户服务,获取当前的用户信息;再调用商品接口获取商品的详细信息。

就以这样一个示例程序来说明吧。

java同步异步阻塞非阻塞 java异步非阻塞实现方式_java

假设这个订单服务调用用户服务的时间是2秒,调用商品服务的时间是3秒,订单服务自身的处理时间是1秒,那整个处理流程所需的时间就是2+3+1=6秒。

1.1 样例代码

用户服务:

/**
 * 模拟的用户服务 
 */
@RestController
@RequestMapping("/user")
public class UserService {

  /** 测试预期值 */
  public static final String CHECK_USER_ID = "1001";

  @RequestMapping(
      value = "/getUserInfo",
      method = {RequestMethod.POST})
  public ApiResponse getUserInfo(@RequestBody UserDTO input) {
    if (null != input && CHECK_USER_ID.equals(input.getUserId())) {
      UserDTO rsp = new UserDTO();
      rsp.setName("bug_null");
      rsp.setAddress("this is shanghai");
      rsp.setUserId(CHECK_USER_ID);

      ThreadUtils.sleep(5);

      return ApiResponse.ok(rsp);
    }
    return ApiResponse.fail();
  }
}

/**
 * 用户的传输实体
 */
public class UserDTO {
  /** 用户的id */
  private String userId;

  /** 用户的名称 */
  private String name;

  /** 地址信息 */
  private String address;

 ......
}


/**
 * 用户服务的springboot的入口
 */
@SpringBootApplication
public class UserApplication {
  public static void main(String[] args) {
    String[] argsNew = new String[] {"-- server.port=9000"};
    SpringApplication.run(UserApplication.class, argsNew);
  }
}

商品服务

/**
 * 用户模拟商品服务
 */
@RestController
@RequestMapping("/goods")
public class GoodsService {

  /** 商品的id */
  public static final String GOODS_ID = "2001";

  @RequestMapping(
      value = "/getGoodsInfo",
      method = {RequestMethod.POST})
  public ApiResponse getUserInfo(@RequestBody GoodsDTO input) {
    if (null != input && GOODS_ID.equals(input.getDataId())) {
      GoodsDTO goods = new GoodsDTO();
      goods.setDataId(GOODS_ID);
      goods.setGoodsPrice(1024);
      goods.setMessage("这是一个苹果,apple,还被咬了一口");

      ThreadUtils.sleep(10);

      return ApiResponse.ok(goods);
    }
    return ApiResponse.fail();
  }
}

/**
 * 商品信息
 */
public class GoodsDTO {

  /** 商品的id */
  private String dataId;

  /** 商品的价格 */
  private Integer goodsPrice;

  /** 商品的描述 */
  private String message;

  ......
}

@SpringBootApplication
public class GoodsApplication {

  public static void main(String[] args) {
    String[] argsNew = new String[] {"-- server.port=9001"};
    SpringApplication.run(GoodsApplication.class, argsNew);
  }
}

订单服务

@RestController
@RequestMapping("/order")
public class OrderServerFacade {

  private Logger logger = LoggerFactory.getLogger(OrderServerFacade.class);

  private Gson gson = new Gson();

  /** 获取连接处理对象 */
  private RestTemplate restTemplate = this.getRestTemplate();

  @RequestMapping(
      value = "/orderInfo",
      method = {RequestMethod.POST})
  public ApiResponse getUserInfo(@RequestBody OrderDTO order) {
    logger.info("getUserInfo start {}", order);
    // 用户信息
    ClientUserDTO userRsp = this.getUserInfo(order.getUserId());
    // 获取商品信息
    ClientGoodsDTO clientGoodRsp = this.getGoods(order.getGoodId());
    ThreadUtils.sleep(1);
    OrderDTO orderRsp = this.builderRsp(userRsp, clientGoodRsp);
    logger.info("getUserInfo start {} rsp {} orderRsp {}  ", order, orderRsp);
    // 构建结果的响应
    return ApiResponse.ok(orderRsp);
  }

  /**
   * 构造响应
   *
   * @param userInfo 用户信息
   * @return 当前响应的用户信息
   */
  private OrderDTO builderRsp(ClientUserDTO userInfo, ClientGoodsDTO goodsInfo) {
    OrderDTO order = new OrderDTO();
    order.setUserId(userInfo.getUserId());
    order.setUserInfo(userInfo);
    order.setGoodId(goodsInfo.getDataId());
    order.setGoodsInfo(goodsInfo);
    return order;
  }

  /**
   * 获取用户的信息
   *
   * @param userId 用户的id
   * @return 用户的信息
   */
  private ClientUserDTO getUserInfo(String userId) {

    logger.info("request get user info start {} ", userId);

    ClientUserDTO clientUser = new ClientUserDTO();
    clientUser.setUserId(userId);
    HttpHeaders headers = new HttpHeaders();
    // 将对象装入HttpEntity中
    HttpEntity<ClientUserDTO> request = new HttpEntity<>(clientUser, headers);
    ResponseEntity<String> result =
        restTemplate.postForEntity("http://localhost:9000/user/getUserInfo", request, String.class);
    if (HttpStatus.OK.value() == result.getStatusCodeValue()) {
      ApiResponse<ClientUserDTO> data =
          gson.fromJson(result.getBody(), new TypeToken<ApiResponse<ClientUserDTO>>() {}.getType());
      // 如果操作成功,则返回结果
      if (data.getResult()) {
        ClientUserDTO rsp = data.getData();
        logger.info("request get user info start {} rsp {} ", userId, rsp);
        return rsp;
      }
    }
    return null;
  }

  /**
   * 获取商品信息
   *
   * @param dataId 商品信息
   * @return 当前的用户的信息
   */
  private ClientGoodsDTO getGoods(String dataId) {

    logger.info("request goods start {} ", dataId);

    ClientGoodsDTO clientGoods = new ClientGoodsDTO();
    clientGoods.setDataId(dataId);
    HttpHeaders headers = new HttpHeaders();
    // 将对象装入HttpEntity中
    HttpEntity<ClientGoodsDTO> request = new HttpEntity<>(clientGoods, headers);
    ResponseEntity<String> result =
        restTemplate.postForEntity(
            "http://localhost:9001/goods/getGoodsInfo", request, String.class);
    if (HttpStatus.OK.value() == result.getStatusCodeValue()) {
      ApiResponse<ClientGoodsDTO> data =
          gson.fromJson(
              result.getBody(), new TypeToken<ApiResponse<ClientGoodsDTO>>() {}.getType());
      // 如果操作成功,则返回结果
      if (data.getResult()) {
        ClientGoodsDTO goodsRsp = data.getData();
        logger.info("request goods start {} , response {} ", dataId, goodsRsp);
        return goodsRsp;
      }
    }
    return null;
  }

  /**
   * 获取连接管理对象
   *
   * @return
   */
  private RestTemplate getRestTemplate() {
    return new RestTemplate(getClientHttpRequestFactory());
  }

  /**
   * 获取连接处理的工厂信息
   *
   * @return
   */
  private SimpleClientHttpRequestFactory getClientHttpRequestFactory() {
    SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();
    clientHttpRequestFactory.setConnectTimeout(25000);
    clientHttpRequestFactory.setReadTimeout(25000);
    return clientHttpRequestFactory;
  }
}

1.2 测试

执行单元测试:

/**
 * 订单服务查询
 *
 */
@RunWith(SpringRunner.class)
@SpringBootTest(
    classes = {OrderApplication.class},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestOrderServerFacade {

  @Autowired private Gson gson;

  @Autowired protected TestRestTemplate restTemplate;

  @Test
  public void testUser() {
    OrderDTO orderInfo = new OrderDTO();
    orderInfo.setGoodId("2001");
    orderInfo.setUserId("1001");
    // 将对象装入HttpEntity中
    HttpEntity<OrderDTO> request = new HttpEntity<>(orderInfo);
    ResponseEntity<String> result =
        restTemplate.postForEntity("/order/orderInfo", request, String.class);
    Assert.assertEquals(HttpStatus.OK.value(), result.getStatusCodeValue());
    ApiResponse<OrderDTO> data =
        gson.fromJson(result.getBody(), new TypeToken<ApiResponse<OrderDTO>>() {}.getType());
    System.out.println(data);
    Assert.assertEquals(data.getResult(), Boolean.TRUE);
    Assert.assertEquals(data.getCode(), APICodeEnum.SUCCESS.getErrorData().getCode());
    Assert.assertNotNull(data.getData().getGoodsInfo());
    Assert.assertNotNull(data.getData().getUserInfo());
  }
}

单元测试结果

java同步异步阻塞非阻塞 java异步非阻塞实现方式_java_02

从结果中耗时是16秒多。也证实了在开始时我的一个预期。同步阻塞式调用。由于只能顺序式调用。所以总时间就是所有调用的总时间相加。

1.3 分析

那当以客户端去调用服务时,客户端都在做什么呢?

这个时候可以借助于java的命令,拿到线程的栈信息和java堆信息

D:\run\dump>jps -l
13568 com.liujun.asynchronous.nonblocking.common.goods.GoodsApplication
1456 com.liujun.asynchronous.nonblocking.invoke.synchronous.OrderApplication
14848 org.jetbrains.jps.cmdline.Launcher
13604 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
3508 com.liujun.asynchronous.nonblocking.common.user.UserApplication
5556 org.jetbrains.idea.maven.server.RemoteMavenServer36
6996
12840 org/netbeans/Main
14840
16092 sun.tools.jps.Jps

    
D:\run\dump>jstack -l 1456 > order.threaddump

查看日志

2021-03-09 10:39:01.591  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : getUserInfo start OrderDTO{userId='1001', userInfo=null, goodId='2001', goodsInfo=null}
2021-03-09 10:39:01.591  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : request get user info start 1001 
2021-03-09 10:39:03.596  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : request get user info start 1001 rsp UserDTO{userId='1001', name='bug_null', address='this is shanghai'} 
2021-03-09 10:39:03.597  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : request goods start 2001 
2021-03-09 10:39:13.603  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : request goods start 2001 , response Goods{dataId='2001', goodsPrice=1024, message='这是一个苹果,apple,还被咬了一口'} 
2021-03-09 10:39:14.603  INFO 1456 --- [nio-9010-exec-6] c.l.a.n.i.synchronous.OrderServerFacade  : getUserInfo start OrderDTO{userId='1001', userInfo=null, goodId='2001', goodsInfo=null} rsp OrderDTO{userId='1001', userInfo=UserDTO{userId='1001', name='bug_null', address='this is shanghai'}, goodId='2001', goodsInfo=Goods{dataId='2001', goodsPrice=1024, message='这是一个苹果,apple,还被咬了一口'}} orderRsp {}

从日志可以看任务是顺序式的调用。所以整个的调用时间就是所有执行的累加。

在本例中,配制的信息仅提取了线程的后15位信息,但已经可以定位到调用的线程了,现在去查看线程的调用栈信息.

"http-nio-9010-exec-6" #29 daemon prio=5 os_prio=0 tid=0x000000002129d000 nid=0x3ab0 runnable [0x000000002338c000]
   java.lang.Thread.State: RUNNABLE
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
	at java.net.SocketInputStream.read(SocketInputStream.java:171)
	at java.net.SocketInputStream.read(SocketInputStream.java:141)
	at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
	at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)
	at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
	- locked <0x00000006c5a0a588> (a java.io.BufferedInputStream)
	at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:735)
	at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:678)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1593)
	- locked <0x00000006c5a0a638> (a sun.net.www.protocol.http.HttpURLConnection)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498)
	- locked <0x00000006c5a0a638> (a sun.net.www.protocol.http.HttpURLConnection)
	at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480)
	at org.springframework.http.client.SimpleClientHttpResponse.getRawStatusCode(SimpleClientHttpResponse.java:55)
	at org.springframework.web.client.DefaultResponseErrorHandler.hasError(DefaultResponseErrorHandler.java:55)
	at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:766)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:736)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:670)
	at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:445)
	at com.liujun.asynchronous.nonblocking.invoke.synchronous.OrderServerFacade.getGoods(OrderServerFacade.java:124)
	at com.liujun.asynchronous.nonblocking.invoke.synchronous.OrderServerFacade.getUserInfo(OrderServerFacade.java:50)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	- locked <0x00000006c59c6e10> (a org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- <0x00000006c53ad000> (a java.util.concurrent.ThreadPoolExecutor$Worker)

IO会阻塞在:java.net.SocketInputStream#socketRead0 的native方法上。

通过分析,就可以知道同步调用的一个线程模型:

java同步异步阻塞非阻塞 java异步非阻塞实现方式_java_03

1.4 总结

优点:

这应该是编程最简单的模型了。调用链非常的清楚,简单直接。不需要其他复杂的机制来解决异步所带来的问题,等着结果即可,所以在很大一部分业务场景中,采用的都是此调用模型。

劣势:

同步阻塞的最大问题在于在调用过程中线程处于等待状态,线程的资源没有充分的利用,对于服务器端应用来说,会限并发的用户数。