Servlet 3.0标准新增了诸多特性,异步处理支持是令开发者最为关注的一个特性。本文就将详细对比传统的Servlet与异步Servlet在开发以及性能上的差别,分析异步Servlet为何会提升Java Web应用的性能。在进行性能分析前,先简单介绍什么是同步异步、什么是阻塞非阻塞,以及AIO、NIO、BIO的概念。
基础知识
1 什么是同步与异步、阻塞与非阻塞
同步:自己去银行取钱(JAVA自己处理IO读写)
异步:委托别人拿银行卡去银行取钱,然后给你(Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API)
阻塞:ATM排队取款,只能傻等(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)
非阻塞:柜台取款,取个号,然后坐在椅子上做其它事,广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。
2 什么是BIO、NIO、AIO
2.1 BIO
同步并阻塞,服务器实现模式为一个连接一个线程,每个线程亲自处理io并且一直等待io的完成,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。当然可以通过线程池机制改善,但当线程的数量增加过多,标准 I/O 的不足就表现出来了。由于要在线程间进行上下文切换,因此 CPU 简直变成了超载。
BIO的局限:对每一个客户端的socket连接,IO都需要一个线程来处理,而且在此期间,这个线程一直被占用,直到socket关闭。在这期间,tcp的连接、数据的读取、数据的返回都是被阻塞的,大量的浪费了cpu的时间片和线程占用的内存资源。
2.2 NIO (new IO) 从jdk1.4开始
同步非阻塞。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有I/O请求时才启动一个线程进行处理。
NIO用一个线程来轮询监控多个数据传输通道,哪个通道准备好了(即有了一组可以处理的数据),就处理哪个通道。
例如:服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时,则调用该socket连接的读操作;如果发现某个 Socket端口上有数据可写时,则调用该socket连接的写操作;如果某个端口的Socket连接已经中断,则调用相应的方法关闭该端口。
这样能充分利用服务器资源,效率得到了很大提高。 但是仍存在的一个问题就是需要轮询线程进行不断的轮询查看数据是否准备就绪,因为也会浪费一些系统资源。
2.3 AIO (Asynchronous io、NIO.2) 从jdk1.7开始
异步非阻塞,服务器实现模式为一个有效请求一个线程。每个线程不必处理io,而是交给服务器处理,并且每个线程也不必等待处理的完成,而是当os处理完了以后通知服务器,这时候服务器再启动线程去处理。AIO模式下就不存在轮询带来的系统资源的浪费了,因此效率更高一些。
3 结论
在连接数不多的情况下,传统IO模式编写方便,响应快速,但是随着连接的不断增多,传统的IO模式已不能满足需求,因此出现了非阻塞IO。非阻塞IO操作中处理10000个连接不再需要10000个线程,你可以用1000个线程,甚至100个线程来处理。当发送请求到服务器,服务器把这个请求当作一个请求"事件",并把这个"事件"分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还,因此一个线程就可以异步的处理多个事件。
测试分析
1 测试结果:

2 测试环境:
注:本测试中测试环境和被测试服务在同一台机器上进行,在进行AIO测试的时候,Jmeter并发量过大和本机维护的线程池过大导致本机CPU资源几乎耗尽,从而影响了AIO接口的性能,实际上的AIO与BIO的性能差距要大于本测试结果。
AIO模拟实现:
基于spring boot框架。
1.自定义线程池:

2.异步请求service:

3.spring boot开启异步支持:@EnableAsync

基础知识
1 什么是同步与异步、阻塞与非阻塞
同步:自己去银行取钱(JAVA自己处理IO读写)
异步:委托别人拿银行卡去银行取钱,然后给你(Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API)
阻塞:ATM排队取款,只能傻等(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)
非阻塞:柜台取款,取个号,然后坐在椅子上做其它事,广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。
2 什么是BIO、NIO、AIO
2.1 BIO
同步并阻塞,服务器实现模式为一个连接一个线程,每个线程亲自处理io并且一直等待io的完成,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。当然可以通过线程池机制改善,但当线程的数量增加过多,标准 I/O 的不足就表现出来了。由于要在线程间进行上下文切换,因此 CPU 简直变成了超载。
BIO的局限:对每一个客户端的socket连接,IO都需要一个线程来处理,而且在此期间,这个线程一直被占用,直到socket关闭。在这期间,tcp的连接、数据的读取、数据的返回都是被阻塞的,大量的浪费了cpu的时间片和线程占用的内存资源。
2.2 NIO (new IO) 从jdk1.4开始
同步非阻塞。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有I/O请求时才启动一个线程进行处理。
NIO用一个线程来轮询监控多个数据传输通道,哪个通道准备好了(即有了一组可以处理的数据),就处理哪个通道。
例如:服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时,则调用该socket连接的读操作;如果发现某个 Socket端口上有数据可写时,则调用该socket连接的写操作;如果某个端口的Socket连接已经中断,则调用相应的方法关闭该端口。
这样能充分利用服务器资源,效率得到了很大提高。 但是仍存在的一个问题就是需要轮询线程进行不断的轮询查看数据是否准备就绪,因为也会浪费一些系统资源。
2.3 AIO (Asynchronous io、NIO.2) 从jdk1.7开始
异步非阻塞,服务器实现模式为一个有效请求一个线程。每个线程不必处理io,而是交给服务器处理,并且每个线程也不必等待处理的完成,而是当os处理完了以后通知服务器,这时候服务器再启动线程去处理。AIO模式下就不存在轮询带来的系统资源的浪费了,因此效率更高一些。
3 结论
在连接数不多的情况下,传统IO模式编写方便,响应快速,但是随着连接的不断增多,传统的IO模式已不能满足需求,因此出现了非阻塞IO。非阻塞IO操作中处理10000个连接不再需要10000个线程,你可以用1000个线程,甚至100个线程来处理。当发送请求到服务器,服务器把这个请求当作一个请求"事件",并把这个"事件"分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还,因此一个线程就可以异步的处理多个事件。
测试分析
1 测试结果:
2 测试环境:
2.1 AIO:
jetty9.3.1
jdk8
servlet3.1
2.2 BIO:
jetty7.6.5
jdk6
servlet2.5
2.3 本机环境:
8G、4核、台式机
2.4 测试方法介绍:
http://www.atatech.org/articles/57011
注:本测试中测试环境和被测试服务在同一台机器上进行,在进行AIO测试的时候,Jmeter并发量过大和本机维护的线程池过大导致本机CPU资源几乎耗尽,从而影响了AIO接口的性能,实际上的AIO与BIO的性能差距要大于本测试结果。
AIO模拟实现:
基于spring boot框架。
1.自定义线程池:
2.异步请求service:
3.spring boot开启异步支持:@EnableAsync
结论:
servlet异步模式比起同步的优势,借助官方的一句话就是:
The benefit is not directly about "performance gain". The purpose of those methods is to avoid a request thread (in async mode) from blocking when it reads input (POST) data or writes the document.
BIO模式已不能满足如今高并发的需求,条件具备的情况下,建议升级AIO模式,性能一定爽翻天。