JAVA高性能I/O设计模式

Java中的IO方式

主要分为3种:BIO(同步阻塞)、NIO(同步非阻塞)和AIO(异步非阻塞)。

BIO

同步阻塞模式。在JDK1.4以前,使用Java建立网络连接时,只能采用BIO方式,在服务器端启动一个ServerSocket,然后使用accept等待客户端请求,对于每一个请求,使用一个线程来进行处理用户请求。线程的大部分时间都在等待请求的到来和IO操作,利用率很低。而且线程的开销比较大,数量有限,因此服务器同时能处理的连接数也很低。

NIO

BIO模式中,是“一个Socket一个线程”;而在NIO中则是使用单个或少量的线程来轮询Socket,当发现Socket上有请求时,才为请求分配线程。因此是“一个请求一个线程”。

具体实现就是把Socket通过Channel注册到Selector,使用一个线程在Selector中轮询,发现Channel有读写的事件,就可以分配给其他线程来处理(通常使用线程池)。

AIO

从JDK7开始支持AIO模式。通过AsynchronousServerSocketChannel中注册事件回调函数来处理业务逻辑。当IO操作完成以后,回调函数会被调用。如果传入AsynchronousChannelGroup,可以绑定线程池来处理事件。

关于JDK的实现,Windows平台基于IOCP实现AIO,Linux只有eppoll模拟实现了AIO。

用一句话来总结这三种IO的区别:

  • BIO是一个连接一个线程。
  • NIO是一个请求一个线程。
  • AIO是一个有效请求一个线程。

IO中的几个概念

以银行取款为例: 

  • 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
  • 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);
  • 阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
  • 非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)

 系统I/O 可分为阻塞型, 非阻塞同步型以及非阻塞异步型。

阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里。 结果调用者被阻塞了, 这段时间了做不了任何其它事情。 更郁闷的是,在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿read()操作来说吧, 调用此函数的代码会一直僵在此处直至它所读的socket缓存中有数据到来。

相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试度看吧。比如read()操作, 如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉调用read()者”数据还没准备好,你稍后再试”。

在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(比如通过回调函数)。拿Windows的ReadFile()或者POSIX的aio_read()来说,调用它之后,函数立即返回,操作系统在后台同时开始读操作。

在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。

Reactor and Proactor

一般情况下,I/O 复用机制需要事件分享器(event demultiplexor )。 事件分享器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁的什么东西送了, 快来拿吧。开发人员在开始的时候需要在分享器那里注册感兴趣的事件,并提供相应的处理者(event handlers),或者是回调函数; 事件分享器在适当的时候会将请求的事件分发给这些handler或者回调函数。

涉及到事件分享器的两种模式称为:Reactor and Proactor 。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。

Reactor模式:

Reator类图如上所示,Reactor模式又叫反应器或反应堆,即实现注册描述符(Handle)及事件的处理器(EventHandler),当有事件发生的时候,事件多路分发器(Event Demultiplexer)做出反应,调用事件具体的处理函数(ConcreteEventHandler::handle_event())。

Reator模式的典型启动过程如下:

  1. 创建Reactor
  2. 注册事件处理器(Reactor::register_handler())
  3. 调用事件多路分发器进入无限事件循环(Reacor:handle_events)
  4. 当操作系统通知某描述符状态就绪时,事件多路分发器找出并调用此描述符注册的事件处理器。

 Reactor模式已经被广泛使用,著名的开源事件库libevent、libev、libuv都是使用Reactor模式。

Proactor模式

Proactor模式的类图如上图所示,Proactor模式又叫前摄器或主动器模式。它用于实现异步I/O模型,运行流程如下:

  1. Initiator主动调用Asynchronous Operation Processor发起异步I/O操作,
  2. 记录异步操作的参数和函数地址放入完成事件队列(Completion Event Queue)中
  3. Proactor循环检测异步事件是否完成。如果完成则从完成事件队列中取出回调函数完成回调。

Boost库中的asio就使用了Proactor模式,其底层的异步I/O由操作系统提供,而异步事件的分发还是由epoll/kequeue/select等实现。

在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。

两者区别

主要区别:

  • Reactor实现同步I/O多路分发,Proactor实现异步I/O分发。

如果只是处理网络I/O单线程的Reactor尚可处理,但如果涉及到文件I/O,单线程的Reactor可能被文件I/O阻塞而导致其他事件无法被分发。所以涉及到文件I/O最好还是使用Proactor模式,或者用多线程模拟实现异步I/O的方式。

  • Reactor模式注册的是文件描述符的就绪事件,而Proactor模式注册的是完成事件。

即Reactor模式有事件发生的时候要判断是读事件还是写事件,然后用再调用系统调用(read/write等)将数据从内核中拷贝到用户数据区继续其他业务处理。

而Proactor模式一般使用的是操作系统的异步I/O接口,发起异步调用(用户提供数据缓冲区)之后操作系统将在内核态完成I/O并拷贝数据到用户提供的缓冲区中,完成事件到达之后,用户只需要实现自己后续的业务处理即可。

  • 主动和被动

Reactor模式是一种被动的处理,即有事件发生时被动处理。而Proator模式则是主动发起异步调用,然后循环检测完成事件。

写在最后

我们知道linux系统提供的异步I/O,只支持O_DIRECT,不能带缓存。因此出现了开源库libeio,它和Linux的异步I/O一样也是用多线程模拟,但是更高效。下图是libeio的异步I/O实现,是不是很像Proactor模式啊。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏linux驱动个人学习

Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)

exit是c语言的库函数,他最终调用_exit。在此之前,先清洗标准输出的缓存,调用用atexit注册的函数等, 在c语言的main函数中调用return就等价...

46030
来自专栏京东技术

开发属于自己的插件 | IDEA & Android Studio插件开发指南

谷轩宇——从事安卓开发,目前效力于通天塔技术开放组是否曾经被ide重复繁琐的操作所困扰,又或者没有心仪的UI控件而难受。那么请阅读这篇文章,掌握idea插件的开...

1.1K20
来自专栏月色的自留地

简单上手nodejs调用c++(c++和js的混合编程)

因为项目的原因,最近经常使用node.js搭RESTful接口。 性能还是很不错啦,感觉比Spring Boot之类的要快。而且在不错的性能之外,只要程序...

17640
来自专栏PHP在线

Redis 和 Memcached 的区别

说到redis就会联想到memcached,反之亦然。了解过两者的同学有那么个大致的印象:redis与memcached相比,比仅支持简单的key-value数...

45960
来自专栏个人分享

Spark数据传输及ShuffleClient(源码阅读五)

  我们都知道Spark的每个task运行在不同的服务器节点上,map输出的结果直接存储到map任务所在服务器的存储体系中,reduce任务有可能不在同一台机器...

12930
来自专栏Web项目聚集地

三周学会小程序第五讲:登录的原理和实现

前面我们耗费在环境搭建上面已经很多时间,这一讲开始真正的和小程序功能对接。 登录便是小程序的开始,小程序可以方便的使用微信登录,获取用户的个人信息,这样我们就能...

13920
来自专栏Linyb极客之路

分布式配置中心之Apollo简易环境部署

Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治...

25920
来自专栏Java进阶架构师

9个提升逼格的redis命令

既然keys命令不允许使用,那么有什么代替方案呢?有!那就是scan命令。如果把keys命令比作类似select * from users where user...

14940
来自专栏IT技术精选文摘

JVM致命错误日志(hs_err_pid.log)分析

当jvm出现致命错误时,会生成一个错误文件 hs_err_pid<pid>.log,其中包括了导致jvm crash的重要信息,可以通过分析该文件定位到导致cr...

68550
来自专栏Python中文社区

Python开源项目推荐:跨平台批量运维小工具batch_ops

batch_ops是一个用Python实现的跨平台批量运维小工具。通过它可以执行远程命令/上传下载文件。它基于yaml配置文件可灵活指定操作单位:host(s)...

17030

扫码关注云+社区

领取腾讯云代金券