NIO 入门
目录
Java 平台提供了一整套 I/O 隐喻,其抽象程度各有不同。然而,离冰冷的现实越远,要想搞清楚来龙去脉就越难。
¶概述
❓为什么还需要 NIO?
在了解 NIO 细节之前,理解以下概念是非常重要的:「 缓冲区操作 」、「 内核空间与用户空间 」、「 虚拟内存 」、「 分页技术 」、「 面向文件的 I/O 」和「 流 I/O 」 和 「 多工 I/O(就绪性选择) 」
¶缓冲区操作
所谓「 输入/输出 」讲的无非就是把数据移进或移出缓冲区。进程执行 I/O 操作,归结起来,也就是向操作系统发出请求,让它要么把缓冲区里的数据排干 (写),要么用数据把缓冲区填满(读)。进程使用这一机制处理所有数据进出操作。
上图简单描述了数据从外部磁盘向运行中的进程的内存区域移动的过程。进程使用 read( )系统调用,要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存缓冲区,这一步通过 DMA 完成,无需主 CPU 协助。一旦磁盘控制器把缓冲区装满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行read( )
调用时指定的缓冲区。
当进程请求 I/O 操作的时候,它执行一个系统调用(有时称为陷阱)将控制权移交给内核。 C/C++程序员所熟知的底层函数 open( )、read( )、write( )和 close( )要做的无非就是建立和执行适当 的系统调用。当内核以这种方式被调用,它随即采取任何必要步骤,找到进程所需数据,并把数据 传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能 已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则 进程被挂起,内核着手把数据读进内存
❓ 为什么不直接让磁盘控制器把数据送到用户空间的缓冲区?
许多操作系统能把组装/分解过程进行得更加高效。根据发散/汇聚的概念,进程只需一个系 统调用,就能把一连串缓冲区地址传递给操作系统。然后,内核就可以顺序填充或排干多个缓冲 区,读的时候就把数据发散到多个用户空间缓冲区,写的时候再从多个缓冲区把数据汇聚起来
这样用户进程就不必多次执行系统调用(那样做可能代价不菲),内核也可以优化数据的处理 过程,因为它已掌握待传输数据的全部信息。如果系统配有多个 CPU,甚至可以同时填充或排干 多个缓冲区。
¶虚拟内存
所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件
RAM)内存地址。这样做好处颇多,总结起来可分为两大类:
前一节提到,设备控制器不能通过 DMA 直接存储到用户空间,但通过利用上面提到的第一 项,则可以达到相同效果。把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,这样, DMA 硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区
通过内存空间多重映射,省去了内核与用户空间的往来拷贝,但前提条件是,内核与用户缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制器块大小通常为 512 字节磁盘扇区)的倍 数。操作系统把内存地址空间划分为页,即固定大小的字节组。内存页的大小总是磁盘块大小的倍 数,通常为 2 次幂(这样可简化寻址操作)。典型的内存页为 1,024、2,048 和 4,096 字节。虚拟和 物理内存页的大小总是相同的。
🤔NIO 主要组件
- NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)
✨NIO 特性
- NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同, NIO 支持面向缓冲区的、基于通道的 IO 操作
- NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
- 非阻塞写与非阻塞读相同,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- NIO 是可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况,可以分配 20 或者 80 个线程来处理。不像之前的阻塞 IO 那样,非得分配 1000 个
¶NIO 与 BIO 的比较
✨BIO 与 NIO 的比较
NIO | BIO |
---|---|
面向缓冲区(Buffer) | 面向流(Stream) |
非阻塞(Non Blocking IO) | 阻塞 IO(Blocking IO) |
🎶 NIO 是基于缓冲区的操作,数据总是从通道读取到缓冲区,或者从缓冲区写入通道
¶传统 IO 方式
¶NIO 方式
¶NIO 三大核心原理示意图
NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)
✨三大组件特点
🎶 NIO 的核心在于Channel
和Buffer
,通道表示打开安到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设别的通道以及用于容纳数据的缓冲区,然后操作缓冲区面对数据进行处理
¶Buffer(缓冲区)
✨ 缓冲区具有如下特点
¶Channel(通道)
🆚 通道(channel)与流(stream)的对比:
¶Selector(选择器)
selector 单从字面意思不好理解,需要结合服务器的设计演化来理解它的用途
¶多线程 IO 设计
graph LR subgraph 多线程 IO 设计 t1(thread) --> s1(socket1) t2(thread) --> s2(socket2) t3(thread) --> s3(socket3) end
⚠️ 上述的多线程 IO 设计,会造成系统内存占用过高,线程上下文切换成本高,并且只适合连接数少的场景
¶线程池 IO 设计
graph LR subgraph 线程池 IO 设计 t4(threadpool1) --> s4(socket1) t5(threadpool2) --> s5(socket2) t4(threadpool1) -.-> s6(socket3) t5(threadpool2) -.-> s7(socket4) end
⚠️ 线程池版本的缺点是在阻塞模式下,线程仅能处理一个 socket 连接,并且这种方式仅适合短连接场景
¶selector IO 设计
graph TD subgraph selector IO 设计 thread --> selector selector --> c1(channel) selector --> c2(channel) selector --> c3(channel) end
✨ selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景(low traffic)
¶附录
Java NIO 系列教程
NIO 相关基础篇
Java NIO?看这一篇就够了!
搞定 Java NIO:NIO 面试问题梳理