欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解Servlet 3.0/3.1 中的异步处理

程序员文章站 2024-03-02 22:25:34
在servlet 3.0之前,servlet采用thread-per-request的方式处理请求,即每一次http请求都由某一个线程从头到尾负责处理。如果一个请求需要进行...

在servlet 3.0之前,servlet采用thread-per-request的方式处理请求,即每一次http请求都由某一个线程从头到尾负责处理。如果一个请求需要进行io操作,比如访问数据库、调用第三方服务接口等,那么其所对应的线程将同步地等待io操作完成, 而io操作是非常慢的,所以此时的线程并不能及时地释放回线程池以供后续使用,在并发量越来越大的情况下,这将带来严重的性能问题。即便是像spring、struts这样的高层框架也脱离不了这样的桎梏,因为他们都是建立在servlet之上的。为了解决这样的问题,servlet 3.0引入了异步处理,然后在servlet 3.1中又引入了非阻塞io来进一步增强异步处理的性能。

本文源代码:

项目下载地址:

在servlet 3.0中,我们可以从httpservletrequest对象中获得一个asynccontext对象,该对象构成了异步处理的上下文,request和response对象都可从中获取。asynccontext可以从当前线程传给另外的线程,并在新的线程中完成对请求的处理并返回结果给客户端,初始线程便可以还回给容器线程池以处理更多的请求。如此,通过将请求从一个线程传给另一个线程处理的过程便构成了servlet 3.0中的异步处理。

举个例子,对于一个需要完成长时处理的servlet来说,其实现通常为:

@webservlet("/synchello")
public class synchelloservlet extends httpservlet {

  protected void doget(httpservletrequest request,
             httpservletresponse response) throws servletexception, ioexception {
    new longrunningprocess().run();
    response.getwriter().write("hello world!");
  }
}

为了模拟长时处理过程,我们创建了一个longrunningprocess类,其run()方法将随机地等待2秒之内的一个时间:

public class longrunningprocess {

  public void run() {
    try {

      int millis = threadlocalrandom.current().nextint(2000);
      string currentthread = thread.currentthread().getname();
      system.out.println(currentthread + " sleep for " + millis + " milliseconds.");
      thread.sleep(millis);

    } catch (interruptedexception e) {
      e.printstacktrace();
    }
  }
}

此时的synchelloservlet将顺序地先执行longrunningprocess的run()方法,然后将将helloworld返回给客户端,这是一个典型的同步过程。

在servlet 3.0中,我们可以这么写来达到异步处理:

@webservlet(value = "/simpleasync", asyncsupported = true)
public class simpleasynchelloservlet extends httpservlet {

  protected void doget(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception {
    asynccontext asynccontext = request.startasync();

    asynccontext.start(() -> {
      new longrunningprocess().run();
      try {
        asynccontext.getresponse().getwriter().write("hello world!");
      } catch (ioexception e) {
        e.printstacktrace();
      }
      asynccontext.complete();
    });

  }

此时,我们先通过request.startasync()获取到该请求对应的asynccontext,然后调用asynccontext的start()方法进行异步处理,处理完毕后需要调用complete()方法告知servlet容器。start()方法会向servlet容器另外申请一个新的线程(可以是从servlet容器中已有的主线程池获取,也可以另外维护一个线程池,不同容器实现可能不一样),然后在这个新的线程中继续处理请求,而原先的线程将被回收到主线程池中。事实上,这种方式对性能的改进不大,因为如果新的线程和初始线程共享同一个线程池的话,相当于闲置下了一个线程,但同时又占用了另一个线程。

当然,除了调用asynccontext的start()方法,我们还可以通过手动创建线程的方式来实现异步处理:

@webservlet(value = "/newthreadasync", asyncsupported = true)
public class newthreadasynchelloservlet extends httpservlet {

  protected void doget(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception {

    asynccontext asynccontext = request.startasync();

    runnable runnable = () -> {
      new longrunningprocess().run();
      try {
        asynccontext.getresponse().getwriter().write("hello world!");
      } catch (ioexception e) {
        e.printstacktrace();
      }
      asynccontext.complete();
    };

    new thread(runnable).start();

  }

}

自己手动创建新线程一般是不被鼓励的,并且此时线程不能重用。因此,一种更好的办法是我们自己维护一个线程池。这个线程池不同于servlet容器的主线程池,如下图:

详解Servlet 3.0/3.1 中的异步处理

在上图中,用户发起的请求首先交由servlet容器主线程池中的线程处理,在该线程中,我们获取到asynccontext,然后将其交给异步处理线程池。可以通过java提供的executor框架来创建线程池:

@webservlet(value = "/threadpoolasync", asyncsupported = true)
public class threadpoolasynchelloservlet extends httpservlet {

  private static threadpoolexecutor executor = new threadpoolexecutor(100, 200, 50000l, timeunit.milliseconds, new arrayblockingqueue<>(100));

  protected void doget(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception {

    asynccontext asynccontext = request.startasync();

    executor.execute(() -> {

      new longrunningprocess().run();

      try {
        asynccontext.getresponse().getwriter().write("hello world!");
      } catch (ioexception e) {
        e.printstacktrace();
      }

      asynccontext.complete();

    });
  }

}

servlet 3.0对请求的处理虽然是异步的,但是对inputstream和outputstream的io操作却依然是阻塞的,对于数据量大的请求体或者返回体,阻塞io也将导致不必要的等待。因此在servlet 3.1中引入了非阻塞io(参考下图红框内容),通过在httpservletrequest和httpservletresponse中分别添加readlistener和writerlistener方式,只有在io数据满足一定条件时(比如数据准备好时),才进行后续的操作。

详解Servlet 3.0/3.1 中的异步处理

对应的代码示:

@webservlet(value = "/nonblockingthreadpoolasync", asyncsupported = true)
public class nonblockingasynchelloservlet extends httpservlet {

  private static threadpoolexecutor executor = new threadpoolexecutor(100, 200, 50000l, timeunit.milliseconds, new arrayblockingqueue<>(100));

  protected void doget(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception {

    asynccontext asynccontext = request.startasync();

    servletinputstream inputstream = request.getinputstream();

    inputstream.setreadlistener(new readlistener() {
      @override
      public void ondataavailable() throws ioexception {

      }

      @override
      public void onalldataread() throws ioexception {
        executor.execute(() -> {
          new longrunningprocess().run();

          try {
            asynccontext.getresponse().getwriter().write("hello world!");
          } catch (ioexception e) {
            e.printstacktrace();
          }

          asynccontext.complete();

        });
      }

      @override
      public void onerror(throwable t) {
        asynccontext.complete();
      }
    });


  }

}

在上例中,我们为servletinputstream添加了一个readlistener,并在readlistener的onalldataread()方法中完成了长时处理过程。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。