在前面学习了 IO 之后,今天我们开始进入 NIO 学习环节,首先我们会 NIO 做一个简单的介绍,让大家认识 NIO,然后会和 IO 进行一个对比认识进行区分。好了,下面我们就开始学习:
一、NIO 简介
1. 概述
从 JDK1.4 开始,Java 提供了一系列改进的输入 / 输出处理的新特性,被统称为 NIO(即 New I/O)。新增了许多用于处理输入输出的类,这些类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,新增了满足 NIO 的功能。NIO 采用内存映射文件的方式来处理输入输出,NIO 将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了。
Java NIO(New IO) 是从 Java 1.4 版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同, NIO 支持面向缓冲区的、基于通道的 IO 操作。 NIO 将以更加高效的方式进行文件的读写操作。
2. 缓冲区 Buffer
缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流 I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。具体看下面这张图就理解了:
上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入 Buffer 中,然后将 Buffer 中的内容写入通道。服务端这边接收数据必须通过 Channel 将数据读入到 Buffer 中,然后再从 Buffer 中取出数据来处理。
在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer,对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示:
备注:可以看到出了 Boolean 类型外,其它都有对应的 Buffer.
3. 通道 Channel
Channel 和传统 IO 中的 Stream 很相似。虽然很相似,但是有很大的区别,主要区别为:通道是双向的,通过一个 Channel 既可以进行读,也可以进行写;而 Stream 只能进行单向操作,通过一个 Stream 只能进行读或者写,比如 InputStream 只能进行读取操作,OutputStream 只能进行写操作;
通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过 Buffer 对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
比喻:通常我们把 IO,比喻成为水流,管道就是水流的通道;NIO 中,我们比喻为火车的轨道,然后缓冲区就是上面的火车。
在 NIO 中,提供了多种通道对象,而所有的通道对象都实现了 Channel 接口。它们之间的继承关系如下图所示:
Channel(通道) 表示到实体如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作的程序组件的开放的连接。所有的 Channel 都不是通过构造器创建的,而是通过传统的节点 InputStream、OutputStream 的 getChannel 方法来返回响应的 Channel。
Channel 中最常用的三个类方法就是 map、read 和 write,其中 map 方法用于将 Channel 对应的部分或全部数据映射成 ByteBuffer,而 read 或 write 方法有一系列的重载形式,这些方法用于从 Buffer 中读取数据或向 Buffer 中写入数据。
4. 选择器 Selector
Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
与 Selector 有关的一个关键类是 SelectionKey,一个 SelectionKey 表示一个到达的事件,这 2 个类构成了服务端处理业务的关键逻辑。
二、NIO 和 IO 区别
1. 概述
Java NIO 提供了与标准 IO 不同的 IO 工作方式:
2. 使用场景
3.NIO VS IO
NIO vs IO 之间的理念上面的区别(NIO 将阻塞交给了后台线程执行)
例子比喻:(http://blog.csdn.net/zhouhl_cn/article/details/6568119)
传统的 socket IO 中,需要为每个连接创建一个线程,当并发的连接数量非常巨大时,线程所占用的栈内存和 CPU 线程切换的开销将非常巨大。使用 NIO,不再需要为每个线程创建单独的线程,可以用一个含有限数量线程的线程池,甚至一个线程来为任意数量的连接服务。由于线程数量小于连接数量,所以每个线程进行 IO 操作时就不能阻塞,如果阻塞的话,有些连接就得不到处理,NIO 提供了这种非阻塞的能力。
小量的线程如何同时为大量连接服务呢,答案就是就绪选择。这就好比到餐厅吃饭,每来一桌客人,都有一个服务员专门为你服务,从你到餐厅到结帐走人,这样方式的好处是服务质量好,一对一的服务,VIP 啊,可是缺点也很明显,成本高,如果餐厅生意好,同时来 100 桌客人,就需要 100 个服务员,那老板发工资的时候得心痛死了,这就是传统的一个连接一个线程的方式。
老板是什么人啊,精着呢。这老板就得捉摸怎么能用 10 个服务员同时为 100 桌客人服务呢,老板就发现,服务员在为客人服务的过程中并不是一直都忙着,客人点完菜,上完菜,吃着的这段时间,服务员就闲下来了,可是这个服务员还是被这桌客人占用着,不能为别的客人服务,用华为领导的话说,就是工作不饱满。那怎么把这段闲着的时间利用起来呢。这餐厅老板就想了一个办法,让一个服务员(前台)专门负责收集客人的需求,登记下来,比如有客人进来了、客人点菜了,客人要结帐了,都先记录下来按顺序排好。每个服务员到这里领一个需求,比如点菜,就拿着菜单帮客人点菜去了。点好菜以后,服务员马上回来,领取下一个需求,继续为别人客人服务去了。这种方式服务质量就不如一对一的服务了,当客人数据很多的时候可能需要等待。但好处也很明显,由于在客人正吃饭着的时候服务员不用闲着了,服务员这个时间内可以为其他客人服务了,原来 10 个服务员最多同时为 10 桌客人服务,现在可能为 50 桌,60 客人服务了。
这种服务方式跟传统的区别有两个:
1、增加了一个角色,要有一个专门负责收集客人需求的人。NIO 里对应的就是 Selector。
2、由阻塞服务方式改为非阻塞服务了,客人吃着的时候服务员不用一直侯在客人旁边了。传统的 IO 操作,比如 read(),当没有数据可读的时候,线程一直阻塞被占用,直到数据到来。NIO 中没有数据可读时,read() 会立即返回 0,线程不会阻塞。
NIO 中,客户端创建一个连接后,先要将连接注册到 Selector,相当于客人进入餐厅后,告诉前台你要用餐,前台会告诉你你的桌号是几号,然后你就可能到那张桌子坐下了,SelectionKey 就是桌号。当某一桌需要服务时,前台就记录哪一桌需要什么服务,比如 1 号桌要点菜,2 号桌要结帐,服务员从前台取一条记录,根据记录提供服务,完了再来取下一条。这样服务的时间就被最有效的利用起来了。参考资料:
http://www.cnblogs.com/xiaoxi
学习视频:《尚硅谷》
来源: https://www.cnblogs.com/pony1223/p/8138233.html