本篇文章主要意在整理Servlet的线程模型,帮助大家更好的理解请求在广泛使用的web容器下(基于Servlet的Tomcat服务器)的运行原理。
What is Servlet?
Servlet是Java的服务端框架,可以利用Servlet来编写一个动态服务器(动态主要是区别于单纯的html构建的静态页面),主要基于Http协议。通过Servlet提供的API,我们可以轻松的处理网络请求和与其他服务建立连接(相比于基于Socket编程),并且基于Java使得它具有跨平台性、灵活性。简单的说Servlet就是一个封装了操作网络请求的API,它将Http网络请求简化为更容易处理的对象。从某种意义上讲,当我们不适用任何web框架(例如Spring mvc和Struts2)时,我们编写的每一个页面(jsp或是继承于HttpServlet的类)也都可以说是一个Servlet。
回忆:一个Servlet编写的例子
让我们回忆一下使用Servlet编写的请求处理,即使现在使用MVC框架的我们一般不会这样做,但是了解其根本的内核仍是很有必要的:
public class DemoServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out =resp.getWriter(); //如果用get方法请求此Servlet,将返回字符串"Hello,world!" out.print("Hello,world!"); out.close(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPut(req, resp); } @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doDelete(req, resp); } }
我们看到了熟悉的方法和对象,即使现在已经有新的web框架封装了Servlet,本质上依旧使用了doGet、doPost等方法,以及相关的对象HttpServletRequest、HttpServletResponse,除了编写此类外,我们还要维护一个web.xml来标识Servlet到url的映射关系。
请求线程模型
在描述Servlet线程模型之前,让我们确定一些需要了解的并发基础:
使用多线程能够“同时”执行多个任务,但是这并不意味着多线程能优化效率,只有在有IO(文件IO、请求IO、数据库IO等)操作时,多线程才较单线程有更高的执行性能;
非IO密集型任务的执行使用多线程,由于时间片的频繁切换和线程创建的损耗,线程越多,执行效率越低,反而串行更加便捷快速;
多线程相较单线程的串行执行相同的任务内容会造成更大的内存开销;
同样使用多线程,基于线程池优化线程的复用会可以有效提高效率、节省资源。
综合以上,我们可以认为,如果一个请求中没有IO,那么使用并发模型执行请求反而会更糟糕。
Servlet演进了很多版本的线程模型,其目的不是提高请求性能,而是为了减小资源开销,提高请求的吞吐量。
随着Servlet版本的演进,Servlet(或者说网络请求的处理)具有三种线程模型(Servlet Threading Model),注意这里的线程模型都是参考Spring-mvc和Tomcat:
Thread per connection
这是Servlet最传统的线程模型,不加限制的为每一个请求分配一个独立的线程,是BIO模型,在线程足够(用户数不多,请求量小于jvm约束的最大线程数)的情况下,不会对新请求产生阻塞,但是毫无疑问这种模型在同一时间内可能有很多线程空闲,浪费资源。大致如下图
Thread per request
Servlet2.5版本支持NIO后的线程模型,不再是单纯的将所有请求绑定在线程上,而是会在调度线程(用于承接请求的线程,这一线程数非常小甚至可能是单线程)进行连接(connect),解析http消息后将线程任务丢给线程池执行,这样能在请求的解析过程中大大提高线程的复用从而减少资源损耗。当然这种模型不适合所有的应用,因为该线程池默认大小可能不能满足很多应用的需求,譬如需要保持长连接的游戏应用,默认200容量的线程池不足以担负起所有的请求任务,会引起阻塞。示意如下,其中disap:
Asynchronous Request handling(异步请求)
Servlet3.0版本已经支持了Asynchronous Request Processing这一异步支持,你可以在请求中返回一个Callable对象,使得能够新建线程执行任务(也是隐式的利用了线程池),同时当前的执行线程得以返回并去处理其他请求(但是仍然在监听Callable对象的返回),以免阻塞执行线程。例如:
@RequestMapping("test") public Callable<String> test(){ return new Callable<String>() { @Override public String call() throws InterruptedException { Thread.sleep(10000); logger.info(Thread.activeCount()+""); return "helo"; } }; }
但是要注意,采用如上的Callable异步请求一般是基于有高IO,后台请求预计运行时间长的情况,因为2.1.0.RELEASE版本之后Callable执行的默认线程池大小只有8,并且有严格的超时机制,单纯的给所有请求都套上一个Callable是不可取的,同时我们最好修改一下默认的Callable线程池:
@Configuration //在稍早些版本这一继承的类是WebMvcConfigurerAdapter public class CallableRequestConfig implements WebMvcConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor(); threadPoolExecutor.setCorePoolSize(150); threadPoolExecutor.setMaxPoolSize(500); threadPoolExecutor.initialize(); //强烈建议调整线程池 configurer.setTaskExecutor(threadPoolExecutor); //设置监听超时时间 单位ms configurer.setDefaultTimeout(60000); configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor()); } @Bean public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor(){ return new TimeoutCallableProcessingInterceptor(); } }
异步请求的方式也不只有return Callable,还有很多其他的实现方式,但最终的处理是大同小异的。有关这一过程的图示(转自csdn):
异步请求的注意事项
首先,正常的业务不是很需要异步请求,除非是那种长请求特别多的情景下,否则默认的处理线程池基本可以代表一个节点系统的并发吞吐量,一般说来更明智的选择无疑是横向扩展机器。当然如果有很多长请求,比如耗时时间很长的查询、文件IO或者计算,我们意识到可能会阻塞这200个线程的调度线程池时,可以按需使用异步请求,再次重申这目的并不是提高请求的性能而是为了节省资源提高系统吞吐量。
并且这样的异步请求在Springboot2.1.0.RELEASE及之后版本中调整了实施措施,在之前的版本中,是不用线程池的新开名为MvcAsync的线程处理任务,当然这个最好也加以限制。同时在之后的版本中必须按如上代码调整线程池,否则异步虽然让出了其他请求的处理线程却会让长请求阻塞(因为异步使用了线程池处理,而异步线程池的有效线程数只有8)。下面借去了两端并发请求的日志,可以对比一下日志中输出的Thread name:
附录:Tomcat与Servlet等的对应关系
Servlet Spec | JSP Spec | EL Spec | WebSocket Spec | Authentication (JASIC) Spec | Apache Tomcat Version | Latest Released Version | Supported Java Versions |
---|---|---|---|---|---|---|---|
5.0 | 3.0 | 4.0 | 2.0 | 2.0 | 10.0.x | 10.0.0-M3 (alpha) | 8 and later |
4.0 | 2.3 | 3.0 | 1.1 | 1.1 | 9.0.x | 9.0.33 | 8 and later |
3.1 | 2.3 | 3.0 | 1.1 | 1.1 | 8.5.x | 8.5.53 | 7 and later |
3.1 | 2.3 | 3.0 | 1.1 | N/A | 8.0.x (superseded) | 8.0.53 (superseded) | 7 and later |
3.0 | 2.2 | 2.2 | 1.1 | N/A | 7.0.x | 7.0.103 | 6 and later (7 and later for WebSocket) |
2.5 | 2.1 | 2.1 | N/A | N/A | 6.0.x (archived) | 6.0.53 (archived) | 5 and later |
2.4 | 2.0 | N/A | N/A | N/A | 5.5.x (archived) | 5.5.36 (archived) | 1.4 and later |
2.3 | 1.2 | N/A | N/A | N/A | 4.1.x (archived) | 4.1.40 (archived) | 1.3 and later |
2.2 | 1.1 | N/A | N/A | N/A | 3.3.x (archived) | 3.3.2 (archived) | 1.1 and later |