概述
知识框架

简介
Java I/O系统 是什么?
全称:
Input/Output
定义:
IO
系统全称为输入输出系统,通常应用于 [设备之间]{.red} 进行数据传输设备:通常交换数据的设备在 [文件和进程之间(文件编程)、客户端和服务器之间(网络编程)]{.red}
细节:
I/O
的主要应用场景就是网络编程,[用于客户端和服务器之间传递数据,而不是用于读取文件的]{.blue},文件已经被数据库取代Tomcat
是应用服务器,仅能够解析HTTP
请求,底层理所应当地是采用 [I/O + 网络编程]{.red} 实现的Netty
是通信框架,能够自定义解析各种各样的请求,底层自然也是采用 [I/O + 网络编程]{.red} 实现的
Java I/O 发展历史
BIO(Blocking I/O)
:JDK 1.0
提供最基本的 [阻塞式]{.red}IO
框架NIO(New I/O)
:JDK 1.4
进一步提供 [非阻塞式]{.red} 的IO
框架AIO(Asynchronous I/O)
:JDK 1.7
提供额外的 [异步式]{.red} 的IO
框架

编码格式
编码格式
前提:
IO
框架涉及到字符和字节的转换,就有必要了解字节的编码格式定义:编码就是把字符转换成字节表示并存储,解码就是把字节转换成字符表示
字符集和编码规则:
- 字符集:
Unicode
- 编码规则:
UTF-8
、UTF-16
、UTF-32
、ASCII
、GB2312
、GBK
、GB18030
- 字符集:
历史:
ASCII 编码:早期计算机仅有英文国家使用,所以仅采用 [7 bit (128)]{.red} 对英文字符进行编码,后来随着使用计算机的国家变多,[8 bit (256)]{.red} 全被用于编码使用
GB2312 编码:ASCII 编码没有足够的位数给中文字符编码,所以中国抛弃了后面的 128 个字符,[规定 两个 ASCII 编码大于 127 字符连接在一起就表示汉字]{.red}
GBK、GB18030 编码:随着时代发展不能够提供足够使用汉字,所以又在 GB2312 的基础上进行改进,得到了 GBK、GB18030 等国标码
[注:GB2312 等国标码都是在 ASCII 码的基础上扩展而来的]{.red}
UTF-8、UTF-16编码:
- 每个国家为了在计算机中使用自己的文字,都制定了只有自己才能够使用的编码方案,不利于各个国家的交流,所以最后 ISO 制定了 Unicode 字符集,为世界上每个字符都分配了编码
- UTF-8、UTF-16 则是具体的编码方案,将每 [8 bit、16bit]{.red} 组成一个编码单元,并且使用特定的标志位为计算机提供信息
UTF-8
范围:0x0000 ~ 0x10FFFF
规则:根据每个字符的 Unicode 编码找到对应的编码格式,然后将二进制的 Unicode 编码分别填入 x 中,最后就得到了 UTF-8 编码
0x0000 ~ 0x007F: 0xxxxxxx
0x0080 ~ 0x07FF: 110xxxxxx 10xxxxxx
0x0800 ~ 0xFFFF: 1110xxxx 10xxxxxx 10xxxxxx
0x10000 ~ 0x10FFFF:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
[知] Unicode 编码为 30693,二进制编码为 1110111 11100101, 填入第三行的编码格式得到 UTF-8 编码 11100111 10011111 10100101,采用十六进制表示:E79FA5
字节数量
UTF-8 UTF-16 GBK 英文英文字符 1B
2B
1B
汉字汉字字符 3B
2B
2B
问题
为什么 UTF-8 和 UTF-16 对于中文编码的字节数量不一样?
UTF-8 提供的标志位更多,每个编码单元能存储的实际信息相对 UTF-16 要少,所以需要采用更多的字节来编码中文
字符集和编码规则有什么区别?
字符集只是对每个不同的字符 [分配唯一的编号(码位、码点)]{.red}:[知] Unicode 编号为 30693;十六进制表示为 0x77E5
编码规则则是根据 [特定的方式]{.red} 将字符的编号转换成二进制进行存储:[知] 11100111 10011111 10100101
你可以尝试将二进制再换算成十六进制,得到的值肯定是不一样的,这就是因为具体的编码规则不同导致的,这里采用是 UTF-8
[注:字符集是对全世界的字符编码的规范,编码格式则是存储字符的具体实现]{.red}
为什么要分别提供字符集和编码规则呢?
如果直接将字符集提供的编号转换成二进制存储就会出现问题:[每个字符编号长度不同就会造成计算机在读取时的歧义]{.red}
[知] 二进制表示为 1110111 11100101 [W] 二进制表示为:1110111:显然计算机就会直接将 [知] 的前半部分解读为 [W],这是显然错误的
为了正确解读每个字符的编号显然需要使用 [标志位]{.red},从而让计算机意识到解读开始和解读结束
因为 [标志位]{.red} 设计的不同就导致诞生了不同格式的编码规则
为什么已经存在 UTF-8 等国际化的编码,中文编码仍在使用
等待更新…
细节:[Java 虚拟机内部采用 UTF-16 编码,字节码采用 UTF-8 编码、源代码可以自行设定编码格式]{.red}
同步与阻塞
阻塞、非阻塞、同步、异步
:::info
怎样理解阻塞非阻塞与同步异步的区别?(这个知乎上有个回答基本解决了我的疑惑)
:::
:::warning
I/O 的同步、异步的概念和并发中的同步、异步的概念是完全不同的
:::
① 先来了解数据进程(程序)究竟是如何通过 I/O 获取到数据的

前提:[理解这幅图需要 操作系统 的相关知识]{.red}
I/O 流程
发起请求:
进程使用 recvfrom 系统调用发起 I/O 请求(应用程序层面的 I/O 请求)
操作系统 查找相应驱动程序发起 I/O 请求 (操作系统层面的 I/O 请求)
控制器完成实际的 I/O 操作
接收数据:
- 网络或者磁盘中的数据经由总线传输进入内核缓冲区中:操作系统层面的 I/O 操作完成
- 内核缓冲区中的数据拷贝进入进程所占用的内存空间中:应用程序层面的 I/O 操作完成
同步与阻塞
- 应用程序发起 I/O 请求实际涉及到两次 I/O 操作:一次是操作系统层面的,一次是应用程序层面的
- 那么我们在讨论同步和阻塞的时候应该将这两次 I/O 操作分开讨论,混为一谈容易导致概念混乱
- 操作系统层面的 I/O 操作:现代操作系统的 I/O 操作全部都是异步非阻塞式的,因为效率高
- 应用程序层面的 I/O 操作:现代操作系统提供给程序员的方法都是同步式的(阻塞或者非阻塞),为了程序员编程的便利
- 因为操作系统层面的 I/O 操作的设计已经固定,所以通常不去讨论,接下来所有的讨论都是针对应用程序层面的
② 再来了解阻塞、非阻塞、同步、异步的概念
阻塞和非阻塞
- 定义:阻塞和非阻塞描述的是 [进程的状态]{.red}
- 阻塞:请求发送方在请求结果返回之前都 [不完成其他任何任务]{.red}
- 非阻塞:请求发送方在请求调用结果返回之前 [可以完成其他任务]{.red}
同步和异步
- 定义:同步和异步描述的是 [进程之间协作完成事件的动作]{.red}
- 同步:[请求接收方为了和请求发送方保持一致性,在处理完请求之前不会返回任何结果]{.red}
- 异步:
- [请求接收方不想和请求发送方保持一致性,那么就会立刻返回确认请求]{.red}
- [请求发送方在接收到确认请求后就可以去完成其他的任务]{.red}
- [请求接收方处理结束后就利用事件通知请求发送方,请求发送方就利用回调函数继续处理]{.red}
区别:
- 同步、异步 与 阻塞、非阻塞的关系
- 同步和异步表示的 [行为]{.red} <-> 阻塞和非阻塞表示的是 [状态]{.red}
- 同步和异步是从 [接收方]{.red} 的角度看待的 <-> 阻塞和非阻塞是从 [发送方]{.red} 的角度看待的
- 同步和异步的区别:
- 同步:应用程序层面的 I/O 操作始终必须由自己主导完成
- 异步:应用程序层面的 I/O 操作完全交给操作系统一起完成,自己只需要等待数据进入自己的内存空间就行
- 同步、异步 与 阻塞、非阻塞的关系
组合:[两组概念可以两两任意组合]{.red}
- 同步阻塞:
- 请求发送方等待返回结果,确保和请求接收方的一致性
- 采用这种组合就会导致请求发送方在等待的过程中什么都不做,工作效率低
- 同步非阻塞:
- 请求发送方等待返回结果的期间会不停地询问请求接收方,确保能及时接收到返回结果保持一致性
- 采用这种组合就会导致发送方长时间运行,[导致处理器满负荷运行,非常占用系统资源]{.red}
- 异步非阻塞:
- 请求发送方在发送请求之后就不再关心返回结果,只需要等待请求接收方返回结果
- 异步阻塞:理论存在的情况,实际并没有人使用
- 同步阻塞:
③ 同步与阻塞的相关问题
- 同步和阻塞、异步和非阻塞有必然的联系吗?
- 同步和异步是进程间协作的方式,确实可以导致进程出现阻塞或者非阻塞的状态。
- [但是并没有必然的联系]{.blue},同步不一定导致进程阻塞,异步也不一定导致进程处于非阻塞。
- 操作系统层面是异步非阻塞式操作,但是应用程序层面使用同步式调用难道不会导致操作系统阻塞吗?
- 同步式调用只会导致请求发送方出现阻塞的状态,请求接收方是不会出现的
- 操作系统由于采用的异步非阻塞式的 I/O 操作,所以在 I/O 控制器没有完成之前,操作系统是可以继续做其他事情的
- 应用程序采用的是同步式调用,始终在等待操作系统返回相应的结果,操作系统在没有收到数据前也是不会返回结果的
- 所以等待的始终只会是进程而不是操作系统
④ 总结
- 阻塞&非阻塞 与 同步&异步仅仅只是看待问题的角度不同而已
- 不存在说同步&异步是操作系统层面的概念,阻塞&非阻塞是进程层面的概念
- 详细分析 IO 流程,显然是有两次 I/O 操作的,而在这两次 I/O 操作中是可以根据需要选择组合方案的
- 只不过现代操作系统通常采用异步非阻塞式、提供给应用程序使用的通常是同步式调用
- 实际上,应用程序也可以直接使用操作系统的异步非阻塞式调用
Linux 通信模型
Linux 通信模型
:::info
:::
前提:
- Linux 通信模型是对上面概念的实际实现,能够帮助更好的理解同步与阻塞的概念
- Linux 通信模型不再涉及对操作系统层面 I/O 操作的讨论,因为基本都是异步非阻塞式的
- [五种通信模型都是操作系统层面的模型,Java 通信模型是在操作系统层面的通信模型的封装]{.blue}
同步 I/O 模型
阻塞式 I/O 模型
进程使用
recvfrom
同步阻塞式方法发起 I/O 请求等待数据进入内存中:进程变为阻塞态网卡或者磁盘中的数据进入操作系统的缓冲区中:数据到达缓冲区
进程解除阻塞状态继续执行
recvfrom
同步阻塞式的方法:缓冲区中的数据进入进程所属内存中
非阻塞式 I/O 模型
- 进程设置为同步非阻塞:进程不会进入阻塞态
- 进程不停地使用
recvfrom
同步阻塞式方法,询问操作系统数据是否到达缓冲区中:进程始终处于运行态 - 网卡或者磁盘中的数据进入操作系统的缓冲区中:数据到达缓冲区
- 进程解除阻塞状态继续执行
recvfrom
同步阻塞式的方法:缓冲区中的数据进入进程所属内存中
[多路复用式 I/O 模型]{.red}:
进程设置为同步非阻塞:[进程不会阻塞在 I/O 操作上而是阻塞在方法上]{.red}
进程使用
select、poll、epoll、kqueue
同步阻塞式的方法:select、poll、epoll、kqueue
方法监视并且不断轮询操作系统,询问操作系统数据是否到达缓冲区中select、poll、epoll、kqueue
方法监视到数据进入操作系统缓冲区后就通知进程+++ 细节
- 这种模型的优势在于可以可以同时处理多个发生的 I/O 事件,而不像阻塞式模型仅能够处理单个 I/O 事件
select
方法采用的是集合存储,poll
方法采用的是链表存储epoll
方法不需要存储多个事件,它为每个事件关联一个句柄,哪个事件发生,它立刻就会返回该事件,而不是一个集合
+++
进程解除阻塞状态继续执行
recvfrom
同步阻塞式的方法:缓冲区中的数据进入进程所属内存中
信号驱动式 I/O 模型:
- 进程使用 sigaction` 方法发起 I/O 请求后收到确认请求,继续执行其他任务:进程处于运行态
- 网卡或者磁盘中的数据进入操作系统的缓冲区中:数据到达缓冲区
- SIGIO 信号处理程序发出信号
SIGIO
通知进程可以开始读取数据 - 进程使用
recvfrom
同步阻塞式方法发起 I/O 请求将数据读取到进程内存中
异步 I/O 模型:
- 进程使用
aio_read
异步非阻塞式的方法发起 I/O 请求后继续执行自己的任务:始终处于运行态 - [网卡或者磁盘中的数据进入操作系统的缓冲区中,操作系统将缓冲区的数据拷贝到进程内存空间]{.red}
- 操作系统通知进程数据已经准备完成可以直接使用
- 进程使用
Java 通信模型
Java 通信模型
前提:Linux 通信模型或者 Java 通信模型都是为了服务器开发而设计的,并不是客户端开发
BIO 编程模型
对应关系:[Linux 阻塞式通信模型]{.red}
工具类:传统 IO 类
服务器设计
单线程模型:
- 传统 IO 类提供的方法都是 [同步阻塞式]{.red} 的,所以单个线程仅能够负责单个用户
- [如果服务器端仅提供单个线程,显然服务器就只能够为单个客户端提供服务,造成服务器端资源的浪费]{.blue}
多线程模型
v1:
解决方案:++为了解决单线程模型的问题最直接的办法就是为每个连接的客户端都提供一个线程,每个线程都负责一个用户++
+++ 缺点
- [大量线程的创建和销毁需要占用非常多的服务器资源]{.green}
- [大量线程的创建和销毁需要占用非常多的服务器资源 大量线程的存在会导致服务器频繁的执行上下文切换,浪费线程的执行时间]{.green}
+++
v2:(伪异步模型)
解决方案:++为了避免v1版的多线程模型下创建销毁线程的开销,最直接的办法就是提供线程池,每个用户仅能够由已存在的线程提供服务++
+++ 缺点
- [大量的线程依然会导致频繁的执行上下文切换]{.green}
- [线程池的线程数量是固定的就会导致在大量请求同时发出时,必然存在客户端长时间等待的情况]{.green}
+++
+++ 优点
[避免服务器创建和销毁大量的线程造成的资源浪费]{.red}
+++
NIO 编程模型
对应关系:
- 原生 NIO 模型:Linux 同步非阻塞式模型
- 原生 NIO 模型 + Selector:Linux 多路复用式模型
工具类:NIO 类
服务器设计:
单线程版
核心:
伪异步模型中使用了线程池依然避免不了大量线程的存在,从而导致频繁的上下文切换,以及用户过长的等待时间
最直接的解决方案依然是仅在客户端发出请求时,服务器端才给予相应的处理
同步非阻塞式模型(轮询模型)
解决方案:++服务器端会不断地询问客户端是否有请求需要处理,如果没有则服务器端继续完成剩下的代码,如果有就处理客户端的请求++
+++ 优点
[减少服务器需要的线程数量,即单个线程就可以完成,避免资源的大量占用和上下文切换占用的时间]{.red}
+++
+++ 缺点
- [单个线程可能来不及 同时 处理多个客户端请求,依然存在客户端等待的情况]{.green}
- [没有任何客户端发出请求时,服务器端存在 空转 的情况,浪费服务器资源]{.green}
+++
多路复用式模型
解决方案:++为了处理轮询模型中出现的服务器端空转现象,最直接的方法就是只有在客户端发出请求的时候服务器端才处理,其余时候阻塞,利用 Selector 类可以做到监视客户端的功能,从而确认当前是否有事件发生++
+++ 优点
- [减少服务器需要的线程数量,即单个线程就可以完成,避免资源的大量占用和上下文切换占用的时间]{.red}
- [服务器不存在空转现象,只在有需要的时候提供服务,其余时候阻塞即可]{.red}
+++
+++ 缺点
[单个线程可能来不及 同时 处理多个客户端请求,依然存在客户端等待的情况]{.green}
+++
-
利用多线程彻底解决此前模型的等待问题
AIO 编程模型
对应关系:Linux 异步非阻塞模型
工具类:AIO 工具类
服务器设计
解决方案:++进程向操作系统注册后继续完成自己的其他任务,操作系统在 IO 事件发生之后会自行寻找 AsynchronousChannelGroup 对象,调用线程池中的线程去执行之前编写好的回调函数,回调函数处理发生的 IO 事件,也就意味着进程在自己不知道的情况下就已经完成了 IO 事件++
+++ 优点
- [异步非阻塞式模型不需要采用 Selector 轮询处理事件的方式而是采用订阅-通知的方式处理事件,效率更高]{.red}
- [Linux 并没有实现真正的异步非阻塞式调用而是采用同步式的 epoll 调用进行模拟的,之前提到过 epoll,确实很类似 AIO]{.red}
- [Windows 下实现了真正的异步非阻塞式调用,采用的是 IOCP 技术]{.red}
+++
+++ 缺点
[程序的编写相对复杂并且不是很好理解]{.green}
+++