NIO
新旧io
- 新io,即NIO:NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。非阻塞 IO
- 旧io: java.io 包中最重要的就是 5 个类和一个接口。5 个类指的是 File、OutputStream、InputStream、Writer、Reader;一个接口指的是 Serializable。都是同步阻塞的,因此,IO 通常也被称为阻塞 IO;面向字节流和字符流
新旧io的区别
- IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
- IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
- NIO是以块的方式处理数据,但是IO是以最基础的字节流的形式去写入和读出的。所以在效率上的话,肯定是NIO效率比IO效率会高出很多。
- NIO不在是和IO一样用OutputStream和InputStream 输入流的形式来进行处理数据的,但是又是基于这种流的形式,而是采用了通道和缓冲区的形式来进行处理数据的。
- 还有一点就是NIO的通道是可以双向的,但是IO中的流只能是单向的。
- NIO的缓冲区(其实也就是一个字节数组)还可以进行分片,可以建立只读缓冲区、直接缓冲区和间接缓冲区,只读缓冲区很明显就是字面意思,直接缓冲区是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。
NIO三大核心对象
channel
- NIO中的channel和IO中的Stream是类似的。只不过Stream是单向的,如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
- 通道是 I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,需要输出的数据被置于一个缓冲区,然后写入通道。对于传回缓冲区的传输,一个通道将数据写入缓冲区中。
- 所有数据都通过 Buffer 对象处理,所以,输出操作时不会将字节直接写入到 Channel 中,而是将数据写入到 Buffer 中;同样,输入操作也不会从 Channel 中读取字节,而是将数据从 Channel 读入 Buffer,再从 Buffer 获取这个字节
- 因为 Channel 是双向的,所以 Channel 可以比流更好地反映出底层操作系统的真实情况。特别是在 Unix 模型中,底层操作系统通常都是双向的。
- 在 Java NIO 中 Channel 主要有如下几种类型:FileChannel:从文件读取数据的;DatagramChannel:读写 UDP 网络协议数据;SocketChannel:读写 TCP 网络协议数据;ServerSocketChannel:可以监听 TCP 连接
buffer
- Buffer 是一个对象,它包含一些要写入或读出的数据。在 NIO 中,数据是放入 Buffer 对象的,而在 IO 中,数据是直接写入或者读到 Stream 对象的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的,如下示意图。

- 在 NIO 中,所有的数据都是用 Buffer 处理的,它是 NIO 读写数据的中转池。Buffer 实质上是一个数组,通常是一个字节数据,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。
- 使用 Buffer 读写数据一般遵循以下四个步骤:1.写入数据到 Buffer;
2.调用 flip() 方法;
3.从 Buffer 中读取数据;
4.调用 clear() 方法或者 compact() 方法。
5.当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 .flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。 - 一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
- Buffer种类:CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ByteBuffer、ShortBuffer、FloatBuffer,上述缓冲区覆盖了我们可以通过 I/O 发送的基本数据类型:characters,double,int,long,byte,short和float

- position 记录当前读取或者写入的位置,写模式下等于当前写入的单位数据数量,从写模式切换到读模式时,置为 0,在读的过程中等于当前读取单位数据的数量;
limit 代表最多能写入或者读取多少单位的数据,写模式下等于最大容量 capacity;从写模式切换到读模式时,等于position,然后再将 position 置为 0,所以,读模式下,limit 表示最大可读取的数据量,这个值与实际写入的数量相等。
capacity 表示 buffer 容量,创建时分配。
此处需要注意buffer的几个方法:flip(), put(),get(),rewind(),allocate(),这几个方法都是重置了position,limit,capacity值
Selectors
- Java NIO引入了多路复用器的概念,它用于监听多个通道的事件。
- 通道和缓冲区的机制,使得 Java NIO 实现了同步非阻塞 IO 模式,在此种方式下,用户进程发起一个 IO 操作以后便可返回做其它事情,而无需阻塞地等待 IO 事件的就绪,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。鉴于此,需要有一个机制来监管这些 IO 事件,如果一个 Channel 不能读写(返回 0),我们可以把这件事记下来,然后切换到其它就绪的连接(channel)继续进行读写。在 Java NIO 中,这个工作由 selector 来完成,这就是所谓的同步。
- Selector 是一个对象,它可以接受多个 Channel 注册,监听各个 Channel 上发生的事件,并且能够根据事件情况决定 Channel 读写。这样,通过一个线程可以管理多个 Channel,从而避免为每个 Channel 创建一个线程,节约了系统资源。如果你的应用打开了多个连接(Channel),但每个连接的流量都很低,使用 Selector 就会很方便。
- 要使用 Selector,就需要向 Selector 注册 Channel,然后调用它的 select() 方法。这个方法会一直阻塞到某个注册的通道有事件就绪,这就是所说的轮询。一旦这个方法返回,线程就可以处理这些事件。
