Java NIO浅析

【摘要】 NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是 I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。    那么NIO的本质是什么样的呢?它是怎样与事件模型结合来解放线程、提高系统吞吐的呢?本文会从传统的阻塞I/O和线程池模型面临的问题讲起,然后对比几种常见 I/…

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是 I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

那么NIO的本质是什么样的呢?它是怎样与事件模型结合来解放线程、提高系统吞吐的呢?

本文会从传统的阻塞I/O和线程池模型面临的问题讲起,然后对比几种常见 I/O模型,一步步分析NIO怎么利用事件模型处理I/O,解决线程池瓶颈处理海量连接,包括利用面向事件的方式编写服务端/客户端程序。最后延展到一些高级主题,如Reactor 与 Proactor模型的对比、Selector的唤醒、Buffer的选择等。

注:本文的代码都是伪代码,主要是为了示意,不可用于生产环境。

让我们先回忆一下传统的服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O)的经典编程模型:

1 {
2 ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
3
4 ServerSocket serverSocket = new ServerSocket();
5 serverSocket.bind(8088);
6 while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
7 Socket socket = serverSocket.accept();
8 executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
9 }
10
11 class ConnectIOnHandler extends Thread{
12 private Socket socket;
13 public ConnectIOnHandler(Socket socket){
14 this.socket = socket;
15 }
16 public void run(){

这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理 I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让 CPU去处理更多的事情。其实这也是所有使用多线程的本质: 1. 利用多核。 2. 当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。

现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲[阻塞队列]一些系统处理不了的连接或请求。

不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在: 1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。 2. 线程本身占用较大内存,像 Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个 JVM的内存都会被吃掉一半。 3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。 4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。

所以,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的 I/O处理模型。

很多刚接触 NIO的人,第一眼看到的就是 Java相对晦涩的API,比如:Channel,Selector,Socket什么的;然后就是一坨上百行的代码来演示 NIO的服务端Demo……瞬间头大有没有?我们不管这些,抛开现象看本质,先分析下NIO是怎么工作的。
常见I/O模型对比

所有的系统 I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。需要说明的是等待就绪的阻塞是不使用 CPU的,是在“空等”;而真正的读写操作的阻塞是使用 CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在 1GB/s级别以上,可以理解为基本不耗时。

下图是几种常见I/O模型的对比:

————————————————

17 while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
18 String someThing = socket.read()….//读取数据
19 if(someThing!=null){
20 ……//处理数据
21 socket.write()….//写数据
22 }
23
24 }
25 }
26 }

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享