一、背景

要提升服务器的并发处理能力,通常有两大方向的思路。

1、系统架构层面。比如负载均衡、多级缓存、单元化部署等等。

2、单节点优化层面。比如修复代码级别的性能Bug、JVM参数调优、IO优化等等。

一般来说,系统架构的合理程度,决定了系统在整体性能上的伸缩性(高伸缩性,简而言之就是可以很任性,性能不行就加机器,加到性能足够为止);而单节点在性能上的优化程度,决定了单个请求的时延,以及要达到期望的性能,所需集群规模的大小。两者双管齐下,才能快速构建出性能良好的系统。

今天,我们就聊聊在单节点优化层面最重要的IO优化。之所以IO优化最重要,是因为IO速度远低于CPU和内存,而不够良好的软件设计,常常导致CPU和内存被IO所拖累,如何摆脱IO的束缚,充分发挥CPU和内存的潜力,是性能优化的核心内容。

而CPU和内存又是如何被IO所拖累的呢?这就从Java中几种典型的IO操作模式说起。

二、Java中的典型IO操作模式

2.1 同步阻塞模式

Java中的BIO风格的API,都是该模式,例如:

Socket socket = getSocket();
socket.getInputStream().read(); //读不到数据誓不返回

该模式下,最直观的感受就是如果IO设备暂时没有数据可供读取,调用API就卡住了,如果数据一直不来就一直卡住。

2.2 同步非阻塞模式

Java中的NIO风格的API,都是该模式,例如:

SocketChannel socketChannel = getSocketChannel(); //获取non-blocking状态的Channel
socketChannel.read(ByteBuffer.allocate(4)); //读不到数据就算了,立即返回0告诉你没有读到

该模式下,通常需要不断调用API,直至读取到数据,不过好在函数调用不会卡住,我想继续尝试读取或者先去做点其他事情再来读取都可以。

2.3 异步非阻塞模式

Java中的AIO风格的API,都是该模式,例如:

AsynchronousSocketChannel asynchronousSocketChannel = getAsynchronousSocketChannel();
asynchronousSocketChannel.read(ByteBuffer.allocate(4), null, new CompletionHandler<Integer, Object>() {@Overridepublic void completed(Integer result, Object attachment) {//读不到数据不会触发该回调来烦你,只有确实读取到数据,且把数据已经存在ByteBuffer中了,API才会通过此回调接口主动通知您
    }@Overridepublic void failed(Throwable exc, Object attachment) {}
});

该模式服务最到位,除了会让编程变的相对复杂以外,几乎无可挑剔。

2.4 小结

对于IO操作而言,同步和异步的本质区别在于API是否会将IO就绪(比如有数据可读)的状态主动通知你。同步意味着想要知道IO是否就绪,必须发起一次询问,典型的一问一答,如果回答是没有就绪,那你还得自己不断询问,直到答案是就绪为止。异步意味着,IO就绪后,API将主动通知你,无需你不断发起询问,这通常要求调用API时传入通知的回调接口。

阻塞和非阻塞的本质区别在于IO操作因IO未就绪不能立即完成时,API是否会将当前线程挂起。阻塞意味着API会一直等待IO就绪后,完成本次IO操作才返回,在此之前调用该API的用户线程将一直挂起,无法进行其他计算处理。非阻塞意味着API会立即返回,而不是等待IO就绪,用户可以立即再次获得线程的控制权,可以使用该线程进行其他计算处理。

那有没有异步阻塞模式呢?如果API支持异步,相当于API说:“你玩去吧,我准备好了通知你”,但是你还是傻乎乎地不去玩,原地等待API做完后的通知。这通常是因为本次IO操作很重要,拿不到结果业务流程根本无法继续,所以为了编程上的简单起见,还是乖乖等吧。可见异步阻塞模式更多的是出于业务流程控制和简化编码难度的考虑,由业务代码自主形成的,Java语言不会特别为你准备异步阻塞IO的API。

三、分离快与慢

3.1 BIO的局限

CPU和内存是高速设备,磁盘、网络等IO设备是低速设备,在Java编程语言中,对CPU和内存的使用被抽象为对线程、栈、堆的使用,对IO设备的使用被抽象为IO相关的API调用。

显然,如果使用BIO风格的IO API,由于其同步阻塞特性,会导致IO设备未就绪时,线程挂起,该线程无法继续使用CPU和内存,直至IO就绪。由于IO设备的速度远低于CPU和内存,所以使用BIO风格的API时,有极大的概率会让当前线程长时间挂起,这就形成了CPU和内存资源被IO所拖累的情况。

作为服务端应用,会面临大量客户端向服务端发起连接请求的场景,每个连接对服务端而言,都意味着需要进行后续的网络IO读取,IO读取完成后,才能获得完整的请求内容,进而才能再进行一些列相关计算处理获得请求结果,最后还要将结果通过网络IO回写给客户端。使用BIO的编码风格,通常是同一个线程全程负责一个连接的IO读取、数据处理和IO回写,该线程绝大部分时间都可能在等待IO就绪,只有极少时间在真正利用CPU资源。

而此时服务器要想同时处理大量客户端连接,后端就同时开启与并发连接数量相应的线程。线程是操作系统的宝贵资源,而且每开启一个操作系统线程,Java还会消耗-Xss指定的线程堆栈大小的堆外内存,如果同时存在大量线程,操作系统调度线程的开销也会显著增加,导致服务器性能快速下降。所以此时服务器想要支持上万乃至几十万的高并发连接,可谓难上加难。

3.2 NIO的突破

3.2.1 突破思路

由于NIO的非阻塞特性,决定了IO未就绪时,线程可以不必挂起,继续处理其他事情。这就为分离快与慢提供了可能,高速的CPU和内存可以不必苦等IO交互,一个线程也不必局限于只为一个IO连接服务。这样,就让用少量的线程处理海量IO连接成为了可能。

3.2.2 思路落地

虽然我们看到了曙光,但是要将这个思路落地还需解决掉一些实际的问题。

a)当IO未就绪时,线程就释放出来,转而为其他连接服务,那谁去监控这个被抛弃IO的就绪事件呢?

b)IO就绪了,谁又去负责将这个IO分配给合适的线程继续处理呢?

为了解决第一个问题,操作系统提供了IO多路复用器(比如Linux下的select、poll和epoll),Java对这些多路复用器进行了封装(一般选用性能最好的epoll),也提供了相应的IO多路复用API。NIO的多路复用API典型编程模式如下:

// 开启一个ServerSocketChannel,在8080端口上监听
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress("0.0.0.0", 8080));
// 创建一个多路复用器
Selector selector = Selector.open();
// 将ServerSocketChannel注册到多路复用器上,并声明关注其ACCEPT就绪事件
server.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() != 0) {// 遍历所有就绪的Channel关联的SelectionKeyIterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();// 如果这个Channel是READ就绪if (key.isReadable()) {// 读取该Channel((SocketChannel) key.channel()).read(ByteBuffer.allocate(10));}if (key.isWritable()) {//... ...
        }// 如果这个Channel是ACCEPT就绪if (key.isAcceptable()) {// 接收新的客户端连接SocketChannel accept = ((ServerSocketChannel) key.channel()).accept();// 将新的Channel注册到多路复用器上,并声明关注其READ/WRITE就绪事件accept.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);}// 删除已经处理过的SelectionKey
        iterator.remove();}
}

IO多路复用API可以实现用一个线程,去监控所有IO连接的IO就绪事件。

第二个问题在上面的代码中其实也得到了“解决”,但是上面的代码是使用监控IO就绪事件的线程来完成IO的具体操作,如果IO操作耗时较大(比如读操作就绪后,有大量数据需要读取),那么会导致监控线程长时间为某个具体的IO服务,从而导致整个系统长时间无法感知其他IO的就绪事件并分派IO处理任务。所以生产环境中,一般使用一个Boss线程专门用于监控IO就绪事件,一个Work线程池负责具体的IO读写处理。Boss线程检测到新的IO就绪事件后,根据事件类型,完成IO操作任务的分配,并将具体的操作交由Work线程处理。这其实就是Reactor模式的核心思想。

3.2.3 Reactor模式

如上所述,Reactor模式的核心理念在于:

a)依赖于非阻塞IO。

b)使用多路复用器监管海量IO的就绪事件。

c)使用Boss线程和Work线程池分离IO事件的监测与IO事件的处理。

Reactor模式中有如下三类角色:

a)Acceptor。用户处理客户端连接请求。Acceptor角色映射到Java代码中,即为SocketServerChannel。

b)Reactor。用于分派IO就绪事件的处理任务。Reactor角色映射到Java代码中,即为使用多路复用器的Boss线程。

c)Handler。用于处理具体的IO就绪事件。(比如读取并处理数据等)。Handler角色映射到Java代码中,即为Worker线程池中的每个线程。

Acceptor的连接就绪事件,也是交由Reactor监管的,有些地方为了分离连接的建立和对连接的处理,为将Reactor分离为一个主Reactor,专门用户监管连接相关事件(即SelectionKey.OP_ACCEPT),一个从Reactor,专门用户监管连接上的数据相关事件(即SelectionKey.OP_READ 和SelectionKey.OP_WRITE)。

关于Reactor的模型图,网上一搜一大把,我就不献丑了。相信理解了它的核心思想,图自然在心中。关于Reactor模式的应用,可以参见著名NIO编程框架Netty,其实有了Netty之后,一般都直接使用Netty框架进行服务端NIO编程。

3.3 AIO的更进一步

3.3.1 AIO得天独厚的优势

你很容易发现,如果使用AIO,NIO突破时所面临的落地问题似乎天然就不存在了。因为每一个IO操作都可以注册回调函数,天然就不需要专门有一个多路复用器去监听IO就绪事件,也不需要一个Boss线程去分配事件,所有IO操作只要一完成,就天然会通过回调进入自己的下一步处理。

而且,更让人惊喜的是,通过AIO,连NIO中Work线程去读写数据的操作都可以省略了,因为AIO是保证数据真正读取/写入完成后,才触发回调函数,用户都不必关注IO操作本身,只需关注拿到IO中的数据后,应该进行的业务逻辑。

简而言之,NIO的多路复用器,是通知你IO就绪事件,AIO的回调是通知你IO完成事件。AIO做的更加彻底一些。这样在某些平台上也会带来性能上的提升,因为AIO的IO读写操作可以交由操作系统内核完成,充分发挥内核潜能,减少了IO系统调用时用户态与内核态间的上下文转换,效率更高。

(不过遗憾的是,Linux内核的AIO实现有很多问题(不在本文讨论范畴),性能在某些场景下还不如NIO,连Linux上的Java都是用epoll来模拟AIO,所以Linux上使用Java的AIO API,只是能体验到异步IO的编程风格,但并不会比NIO高效。综上,Linux平台上的Java服务端编程,目前主流依然采用NIO模型。)

使用AIO API典型编程模式如下:

//创建一个Group,类似于一个线程池,用于处理IO完成事件
AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(Executors.newCachedThreadPool(), 32);
//开启一个AsynchronousServerSocketChannel,在8080端口上监听
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group);
server.bind(new InetSocketAddress("0.0.0.0", 8080));
//接收到新连接
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {//新连接就绪事件的处理函数
    @Overridepublic void completed(AsynchronousSocketChannel result, Object attachment) {result.read(ByteBuffer.allocate(4), attachment, new CompletionHandler<Integer, Object>() {//读取完成事件的处理函数
            @Overridepublic void completed(Integer result, Object attachment) {}@Overridepublic void failed(Throwable exc, Object attachment) {}});}@Overridepublic void failed(Throwable exc, Object attachment) {}
});

3.3.2 Proactor模式

Java的AIO API其实就是Proactor模式的应用。

也Reactor模式类似,Proactor模式也可以抽象出三类角色:

a)Acceptor。用户处理客户端连接请求。Acceptor角色映射到Java代码中,即为AsynchronousServerSocketChannel。

b)Proactor。用于分派IO完成事件的处理任务。Proactor角色映射到Java代码中,即为API方法中添加回调参数。

c)Handler。用于处理具体的IO完成事件。(比如处理读取到的数据等)。Handler角色映射到Java代码中,即为AsynchronousChannelGroup 中的每个线程。

可见,Proactor与Reactor最大的区别在于:

a)无需使用多路复用器。

b)Handler无需执行具体的IO操作(比如读取数据或写入数据),而是只执行IO数据的业务处理。

四、总结

1、Java中的IO有同步阻塞、同步非阻塞、异步非阻塞三种操作模式,分别对应BIO、NIO、AIO三类API风格。

2、BIO需要保证一个连接一个线程,由于线程是操作系统宝贵资源,不可开过多,所以BIO严重限制了服务端可承载的并发连接数量。

3、使用NIO特性,辅以Reactor编程模式,是Java在Linux下实现服务器端高并发能力的主流方式。

4、使用AIO特性,辅以Proactor编程模式,在其他平台上(比如Windows)能够获得比NIO更高的性能。

转载于:https://www.cnblogs.com/itZhy/p/7727569.html

Java进阶知识点5:服务端高并发的基石 - NIO与Reactor模式以及AIO与Proactor模式相关推荐

  1. 服务端高并发分布式架构演进之路(转载,图画的好)

    这个文章基本上从单机版到最终版,经历了加缓存,加机器,高可用,分布式,最后到云等过程,其实我一直想总结一套类似的东西,没想到有人已经先弄出来了,那就不重复造轮子了,而且我感觉这个文章也是花了功夫的. ...

  2. 服务端高并发分布式架构演进之路

    服务端高并发分布式架构演进之路 概述 基本概念 架构演进 单机架构 第一次演进:Tomcat与数据库分开部署 第二次演进:引入本地缓存和分布式缓存 第三次演进:引入反向代理实现负载均衡 第四次演进:数 ...

  3. 服务端高并发分布式架构演进之路(阿里巴巴90秒100亿)

    服务端高并发分布式架构演进之路 阿里巴巴为什么能抗住90秒100亿? 1. 概述 本文以淘宝作为例子,介绍从一百个到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大 ...

  4. 阿里巴巴服务端高并发分布式架构演进之路

    文章目录 1 概述 2 基本概念 2.1 什么是分布式 2.2 什么是高可用 2.3 什么是集群 2.4 什么是负载均衡 2.5 什么是正向代理和反向代理 3 架构演进 3.1 单机架构 3.2 第一 ...

  5. java服务端高并发问题_Java服务端两个常见的并发错误

    理想情况来讲,开发在开始编写代码之前就应该讲并发情况考虑进去,但是大多数实际情况确是,开发压根不会考虑高并发情况下的业务问题.主要原因还是因为业务极难遇到高并发的情况. 下面列举两个比较常见的后端编码 ...

  6. 8000 字 + 21 张图,服务端高并发分布式架构 14 次演进之路

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | huashiou 来源 | sf.gg/a/1 ...

  7. 淘宝服务端高并发分布式架构演进之路

    点击上方"方志朋",选择"置顶公众号" 技术文章第一时间送达! 1. 概述 本文以淘宝作为例子,介绍从一百个并发到千万级并发情况下服务端的架构的演进过程,同时列 ...

  8. 淘宝服务端高并发分布式架构的十四次演进之路

    1.概述 本文以淘宝作为例子,介绍从一百个并发到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构设计的原则. ...

  9. 服务端高并发分布式架构演进

    一. 概述 本文以淘宝作为例子,介绍从一百个到千万级并发情况下服务端的架构的演进过程.同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知.文章最后汇总了一些架构设计的原则. 特 ...

最新文章

  1. MySQL Proxy 安装与读写分离体验
  2. Swift之缓存文件处理
  3. 忆阻器的matlab建模_忆阻器Simulink建模和图形用户界面设计.PDF
  4. 新版手机浏览器_夸克浏览器发布全新3.0版,AI技术创新智能化信息服务
  5. QTableView样式设置
  6. 弱电机房如何理线整理机柜?值得收藏学习
  7. 世界杯的科学---足球的基本原理
  8. 软件测试案例|移动APP非功能性测试
  9. CommonAPI-SomeIP 使用
  10. Hulu斩获两枚艾美奖提名!(附第68届艾美奖重要奖项提名名单)
  11. 中国电商靠低价攻入美国市场,亚马逊已经手足无措
  12. VLDB 2021 COCO 论文阅读
  13. 不会用苹果电脑(mac)的渣渣
  14. 舞象云出席泰华商城智慧营销系统上线发布会,问道传统百货新未来
  15. MindMaster思维导图及亿图图示会员 优惠活动
  16. Sketch for mac(专业矢量绘图设计软件)
  17. 天津出差系列(五)----第五天
  18. MPLAB 创建新项目
  19. 宝塔Linux面板安装SSL证书
  20. 对一支圆珠笔进行测试,要从哪些方面进行测试?

热门文章

  1. 微信公众号下发统一消息
  2. 在 Linux 中查找文件的 4 种方式
  3. 电影“变形金刚”将于7.4.2007进入影院
  4. JavaScript实现文件大小转换、单位转换、toFixed、indexOf、substr、substring、B、KB、MB、GB
  5. BitDock桌面美化工具 一直在后台偷偷上传东西,具体上传什么东西不知,一天耗费我几十个G的流量
  6. 【IDT】 windows IDT GDT LDT
  7. 【翻译】西川善司为了3D游戏粉丝的[生化危机5]图形讲座(后篇)
  8. excel可以做神经网络分析吗,用excel构建神经网络
  9. 车载计算机模块,车载/独显(GPU)计算机系统
  10. 吉他初学者入门(必看 很有帮助)