JavaIO工作的机制
1. 磁盘I/O工作机制
由于读取和写入文件 I/O 操作都调用操作系统提供的接口,因为磁盘设备是由操作系统管理的,应用程序要访问物理设备只能通过系统调用的方式来工作,读和写分别对应 read()
和 write()
两个系统调用。
只要是系统调用就可能存在内核空间地址和用户空间地址切换的问题。
标准访问文件的方式:当写入时,用户的应用程序调用
write()
接口将数据从用户地址空间复制到内核地址空间的缓存中,这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync
同步命令。直接 I/O 的方式:通常直接 I/O 与异步 I/O 结合使用,会得到比较好的性能。
同步访问文件的方式:即数据的读取和写入都是同步操作的,它与标准访问文件的方式不同的是,只有当数据被成功写到磁盘时才返回给应用程序成功的标志。这种访问文件的方式性能比较差,只有在一些对数据安全性要求比较高的场景中会使用,而且通常这种操作方式的硬件都是定制的。
异步访问文件的方式:即当访问数据的线程发出请求之后,线程会接着去处理其他事情,而不是阻塞等待,当请求的数据返回后 继续处理下面的操作。这种访问文件的方式可以明显地提高应用程序的效率,但是不会改变访问文件的效率。
内存映射的方式:指操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据时,转换为访问文件中的某一段数据。这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间的数据是共享的。
数据在磁盘中的唯一最小描述就是文件,也就是说上层应用程序只能通过文件来操作硬盘上的数据,文件也是操作系统和磁盘驱动器交互的最小单元。
值得注意的是,Java 中通常的 File
并不代表一个真实存在的文件对象,当指定一个路径描述符时,它会返回一个代表这个路径的虚拟对象,这个可能是一个真实存在的文件或者是一个包含多个文件的目录。
Java 序列化就是将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。反序列化则是相反的过程,将这个字节数组再重新构造成对象:
- 当需要持久化时,Java对象必须继承
java.io.Serializable
接口。 - 反序列化时,必须有原始类作为模板才能将这个对象还原。由此也可推测,序列化的数据并不像 class 文件那样保存类的完整的结构信息。
在纯 Java 环境下,Java 序列化能够很好地工作,但是在多语言环境下,用 Java 序列化存储后,很难用其他语言还原出结果。在这种情况下,还是要昼存储通用的数据结构,如 JSON 或者 XML 结构数据,当前也有比较好的序列化工具,如 Google 的 protobuf 等。
2. 网络I/O工作机制
搞清楚 TCP 连接的几种状态转换对我们调试网络程序是非常有帮助的:

将一份数据从一个地方正确地传输到另一个地方所需要的时间我们称之为响应时间。
影响网络传输(即响应时间)的因素:
- 网络带宽;
- 传输距离:也就是数据在光纤中;
- TCP拥塞控制:由于TCP传输是一个“停-等-停-等”的协议,传输方和接收受的步调要一致,要达到步调一致就要通过拥塞控制来调节。
建立通信链路:
- 当客户端要与服务端通信时,客户端首先要创建一个
Socket
实例,操作系统将为这个Socket
实例分配一个没有被使用的本地端口号,并创建一个包含本地地址、远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭。在创建Socket
实例的构造函数正确返回之前,将要进行 TCP 的 3 次握手协议,TCP 握手协议完成后,Socket
实例对象将创建完成,否则将抛出IOException
错误。 - 与之对应的服务端将创建一个
ServerSocket
实例,创建ServerSocket
比较简单,只要指定的端口号没有被占用,一般实例创建都会成功。 - 与
ServerSocket
所关联的列表中每个数据结构都代表与一个客户端建立的 TCP 连接。
数据传输(传输数据是我们建立连接的主要目的,下面将详细介绍如何通过 Socket
传输数据):
- 当连接已经建立成功时,服务端和客户端都会拥有一个
Socket
实例,每个Socket
实例都有一个InputStream
和OutputStream
,并通过这两个对象来交换数据。同时我们也知道网络 IO 都是以字节流传输的,当创建Socket
对象时,操作系统会为InputStream
和OutputStream
分别分配一定大小的缓存区,数据的写入和读取都是通过这个缓存区完成的。 - 写入端将数据写到
OutputStream
对应的 SendQ 队列中,当队列填满时,数据将被转移到另一端InputStream
的 RecvQ 队列中,如果这时 RecvQ 已经满了,那么OutputStream
的write
方法将会阻塞,直到 RecvQ 队列有足够的空间容纳 SendQ 发送的数据。 - 特别值得注意的是,这个缓存区的大小及写入端的速度和读取端的速度非常影响这个连接的数据传输效率,由于可能会发生阻塞,所以网络 IO 与磁盘 IO 不同的是数据的写入和读取还要有一个协调的过程,如果在两边同时传送数据可能会产生死锁,在下面的 NIO 部分将介绍如何避免这种情况。