NIO和BIO的区别,NIO的运行原理和并发使用场景,这也是I/O多路复用的基础,越来越多地应用于大型应用服务器,成为解决高并发和并发的有效途径大量连接,I/O处理问题。那么NIO的本质是什么?它如何结合事件模型来解放线程,提高系统吞吐量?本文将从传统的阻塞I/O和线??程池模型所面临的问题入手,然后对比几种常见的I/O模型,一步步分析NIO是如何使用事件模型来处理I/O的,解决了处理海量连接的线程池瓶颈,包括以面向事件的方式编写服务端/客户端程序。***扩展到一些进阶的话题,比如Reactor和Proactor模型的对比,Selector的觉醒,Buffer的选择等。注:本文代码均为伪代码,主要用于说明目的,不得转载在生产环境中使用。传统BIO模型分析让我们先回忆一下传统服务器端同步阻塞I/O处理(即BIO,BlockingI/O)的经典编程模型:{ExecutorServiceexecutor=Executors.newFixedThreadPollExecutor(100);//线程池ServerSocketserverSocket=newServerSocket();serverSocket.bind(8088);while(!Thread.currentThread.isInturrupted()){//主线程无休止地等待新连接的到来Socketsocket=serverSocket.accept();executor.submit(newConnectIOnHandler(socket));//为新连接创建新线程}classConnectIOnHandlerextendsThread{privateSocketsocket;publicConnectIOnHandler(Socketsocket){this.socket=socket;}publicvoidrun(){while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){无限循环处理读写事件StringsomeThing=socket.read()....//读取数据if(someThing!=null){...//处理数据socket.write()....//写入数据}}}}这是经典的per-connectionper-thread模型。使用多线程的主要原因是socket.accept()、socket.read()、socket.write()这三个主要函数都是同步阻塞的。当连接正在处理I/O时,系统将被阻塞。如果是单线程,肯定挂在那里;但是释放了CPU,可以开启多线程让CPU去处理更多的事情。事实上,这就是所有使用多线程的本质:利用多核。当I/O阻塞系统但CPU空闲时,可以使用多线程来使用CPU资源。现在的多线程普遍使用线程池,可以使得创建和回收线程的成本比较低。当活跃连接数不是特别多时(单机小于1000),这种模型比较好,让每个连接专注于自己的I/O,编程模型简单,不用想太多关于系统过载,限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统无法处理的连接或请求。然而,这个模型最根本的问题是它严重依赖线程。但是线程是非常“昂贵”的资源,主要表现在:创建和销毁线程的成本非常高。在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统功能。线程本身占用大量内存,像Java的线程栈,一般至少分配512K到1M的空间。如果系统的线程数超过一千,恐怕整个JVM的内存都要消耗掉一半。线程切换的代价非常高。当操作系统发生线程切换时,需要保存线程的上下文,然后执行系统调用。如果线程数过多,执行线程切换的时间甚至可能比线程执行的时间还要长。这时候表现往往是系统负载高,CPUsyusage特别高(超过20%),导致系统几乎陷入不可用状态。容易造成系统负载参差不齐。因为系统负载是根据活跃线程数或者CPU核数来决定的,一旦线程数很高但是外网环境不是很稳定,很容易造成大量的请求结果同时返回时间,激活大量阻塞线程,造成系统负载压力过大。所以,当面对10万甚至上千的连接时,传统的BIO模型就无能为力了。随着移动应用的兴起和各种网络游戏的盛行,最长连接越来越普遍。这时候就需要一种更高效的I/O处理模型。NIO是如何工作的很多刚接触NIO的人,第一眼看到Java中比较晦涩的API,比如:Channel、Selector、Socket等;然后还有几百行代码来演示NIO服务器Demo……是不是瞬间头疼了?我们先不管这些,抛开现象看本质,先分析一下NIO是如何工作的。1.常见I/O模型比较所有系统I/O都分为两个阶段:等待就绪和运行。比如读取函数分为等待系统可读和真正读取;同样,写入函数也分为等待网卡可以写入和真正写入。需要注意的是,阻塞等待ready不占用CPU,是“空等待”;而真正阻塞的读写操作使用的是CPU,是真正“工作”的,而且这个过程非常快,属于内存复制,带宽通常在1GB/s级别以上,基本可以理解为不费时。下图是几种常见I/O模型的对比:接收数据并返回读取数据。对于NIO,如果TCPRecvBuffer有数据,它会从网卡中读取数据到内存中,返回给用户;否则直接返回0,永不阻塞。最新的AIO(AsyncI/O)会更进一步:不仅等待就绪是非阻塞的,就连网卡到内存的数据传输过程也是异步的。也就是说,在BIO模式下,用户最关心“我想读”,在NIO模式下,用户最关心“我会读”,而在AIO模式下,用户更需要关注“阅读”。NIO的一个重要特点是:socket主要的read、write、register和receive函数在ready阶段是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。2.如何结合事件模型使用NIO的同步非阻塞特性下面我们来看看如何使用事件模型在单线程中处理所有的I/O请求:NIO中主要有几个事件:阅读就绪,写就绪,新连接到达。我们首先需要注册当这些事件到达时对应的处理器。然后在合适的时候告诉事件选择器:我对这个事件感兴趣。对于write操作,当不能写入时,对write事件感兴趣;对于读操作,是在连接完成,系统无法承载新读取的数据时;对于accept,一般是在服务器刚启动的时候;而对于connect,一般是在连接失败需要重连或者直接异步调用连接的时候。其次,使用无限循环选择就绪事件,执行系统调用(select,Linux2.6之前的poll,2.6之后的epoll,IOCPforWindows),阻塞等待新事件的到来。当一个新的事件到来时,一个标志将被注册在选择器上,以指示它是可读的、可写的,或者一个连接即将到来。注意select是阻塞的,无论是通过操作系统通知(epoll)还是不停的轮询(select,poll),这个函数都是阻塞的。所以你可以安全大胆的在while(true)调用这个函数而不用担心CPU空转。所以我们的程序大概是这样的:interfaceChannelHandler{voidchannelReadable(Channelchannel);voidchannelWritable(Channelchannel);}classChannel{Socketsocket;Eventevent;//read,writeorconnect}//IO线程主循环:classIoThreadextendsThread{publicvoidrun(){Channelchannel;while(channel=Selector.select()){//选择就绪事件和对应的连接if(channel.event==accept){registerNewChannelHandler(channel);//如果是新连接,注册一个新的reader写handler}if(channel.event==write){getChannelHandler(channel).channelWritable(channel);//如果可以写入,则执行写入事件}if(channel.event==read){getChannelHandler(channel).channelReadable(channel);//如果可以读取,则执行读取事件}}}Map
