异步处理机制

在Servlet3.0之前,每一次HTTP请求都会由一个Servlet容器的Worker线程从头到尾同步阻塞式的来处理,如果一个请求包含了很多耗时的IO操作,这些Worker线程不能及时释放回线程池,在高并发下就会存在线程耗尽的性能问题。在更早的Servlet版本中唯一办法就是调大Worker线程池的线程数,但这种方案并不能从根本解决问题,调大Worker线程数后的状况可能变成了有更多的Worker线程被阻塞。

Servlet3.0规范引入了对异步处理的支持,异步处理允许Servlet容器在处理客户端请求时不必一直保持Worker线程的占用,而是可以在处理请求的过程中释放Worker线程资源,将耗时的处理交给其它线程。异步Servlet本质上是实现了将Servlet容器宝贵的Worker线程和那些耗时的业务操作解耦,让Worker线程不会被那一小撮耗时业务阻塞耗尽而能够腾出资源处理其它的请求,达到充分利用CPU资源,提高Java应用服务器的并发性能和吞吐量的目的。

本文介绍Servlet3.0规范中如何编写支持异步处理的Servlet组件。

异步处理Servlet组件

下面例子我们编写了一个支持异步处理的Servlet组件,在web.xml中我们配置了<async-supported>属性为true开启Servlet的异步支持。

<servlet>
    <servlet-name>DemoServlet</servlet-name>
    <servlet-class>com.gacfox.demo.demoweb.DemoServlet</servlet-class>
    <async-supported>true</async-supported>
</servlet>

此外,除了使用XML配置开启Servlet异步支持,使用注解方式也是一样的,下面是一个例子。

@WebServlet(name = "DemoServlet", urlPatterns = "/demo", asyncSupported = true)
public class DemoServlet extends HttpServlet {
    // ... 具体业务逻辑
}

一个最简单的异步Servlet组件如下。

package com.gacfox.demo.demoweb;

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("当前线程: " + Thread.currentThread().getId());
        // 在当前请求上下文下创建异步Servlet上下文
        AsyncContext asyncContext = request.startAsync();
        // 设置异步任务超时时间
        asyncContext.setTimeout(5000);
        // 启动异步处理线程
        asyncContext.start(() -> {
            try {
                System.out.println("当前线程: " + Thread.currentThread().getId());
                // 模拟耗时操作
                Thread.sleep(1000);
                // 返回响应
                asyncContext.getResponse().getWriter().write("Complete!");
                // 完成异步操作
                asyncContext.complete();
            } catch (InterruptedException | IOException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

Servlet组件中我们基于AsyncContext创建了异步上下文,并在其中执行耗时操作。使用异步Servlet组件时,一个最佳实践是为所有异步任务指定超时时间,防止异步任务异常的持续运行耗尽系统资源。asyncContext.start()方法接受一个Runnable对象,通过控制台打印的信息我们可以看到Servlet处理线程和异步上下文内部的线程不是同一个线程,耗时操作是在子线程中执行的。上述代码中,主线程会先一步返回,但子线程还在运行,用户的浏览器也会阻塞等待HTTP响应,直到代码中的asyncContext.complete()被调用。

异步上下文监听器

异步上下文支持使用AsyncListener对象来监听异步上下文的开始、完成、超时和异常,这些事件发生时AsyncEvent对象会被传入对应的回调函数,我们可以从其中获取异步上下文等信息。下面例子中,我们使用AsyncListener监听了超时事件。

package com.gacfox.demo.demoweb;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(500);
        asyncContext.start(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("异步处理完成!");
                asyncContext.complete();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        asyncContext.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent event) throws IOException {
            }

            @Override
            public void onTimeout(AsyncEvent event) throws IOException {
                event.getAsyncContext().getResponse().getWriter().write("Timeout!");
                event.getAsyncContext().complete();
            }

            @Override
            public void onError(AsyncEvent event) throws IOException {
            }

            @Override
            public void onStartAsync(AsyncEvent event) throws IOException {
            }
        });
    }
}

代码中,异步操作的超时时间是500ms,而它实际的运行时间超过1000ms,因此异步线程并不会执行完成,“异步处理完成!”字样是永远不会输出的,而onTimeout()函数则会在达到超时时间时被回调,并向Servlet的输出流写入“Timeout!”字样。不过这里注意,我们在onTimeout()内部还调用了asyncContext().complete()方法来完成异步上下文,因此实际上onComplete()会在此被回调。

基于线程池启动异步上下文

前面我们编写的代码是直接启动异步处理线程的,实际开发中对于大量频繁的启动线程最好使用线程池。下面例子中,我们创建了一个大小为20的线程池,并基于这个线程池启动异步上下文并执行耗时操作。

package com.gacfox.demo.demoweb;

import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.*;

public class DemoServlet extends HttpServlet {
    private final ExecutorService executorService = new ThreadPoolExecutor(
            20, 20,
            60L, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(20),
            Executors.defaultThreadFactory()
    );

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(5000);
        // 使用线程池启动异步上下文
        executorService.execute(() -> {
            try {
                Thread.sleep(1000);
                asyncContext.getResponse().getWriter().write("Hello, world!");
            } catch (InterruptedException | IOException e) {
                throw new RuntimeException(e);
            } finally {
                asyncContext.complete();
            }
        });
    }
}

代码中,我们设置线程池大小为20,队列长度为20,它使用线程池的默认拒绝策略。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap