java的IO简述
基础概念
什么是IO
-
IO在计算机中指Input/Output,也就是输入和输出
-
由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口
IO操作
- I/O操作是相对于内存而言的,从外部设备进入内存就叫Input,反之从内存输出到外部设备就叫Output
IO分类
- IO有内存IO、网络IO、磁盘IO,通常我们说的IO指的是后两者
IO处理阶段
- 用户进程空间 <–> 内核空间
- 内核空间 <–> 设备空间(磁盘、网络)
网络IO输入操作的两个阶段
- 等待网络数据到达网卡→把数据读取到内核缓冲区
- 从内核缓冲区复制数据到进程空间
用户空间和内核空间
- 虚拟内存被操作系统划分成两块:内核空间和用户空间。为了安全,它们是隔离的
- 内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态
虚拟内存
- 虚拟内存被操作系统划分成两块:内核空间和用户空间。它们是互相隔离的,即使用户的程序(用户空间)崩溃了,内核也不受影响
- 当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态
- 分类
- 用户空间。用户空间是用户程序代码运行的地方
- 内核空间。内核空间是内核代码运行的地方
缓冲区
阻塞IO和非阻塞IO
- 阻塞 IO指的是需要内核 IO 操作彻底完成后才返回到用户空间执行用户程序的操作指令
- 非阻塞 IO(Non-Blocking IO,NIO) 指的是用户进程不需要等待内核 IO 操作彻底完成,即可返回用户空间执行后续指令。与此同时,内核会立即返回给用户一个 IO 状态值
- 阻塞是指用户进程一直在等待,而不能做别的事情;非阻塞是指用户进程获得内核返回的状态值就返回自己的空间,可以去做别的事情
同步IO和异步IO
- 同步 IO是指用户空间(进程或者线程)是主动发起 IO 请求的一方,系统内核是被动接收方
- 异步 IO则反过来,系统内核是主动发起 IO 请求的一方,用户空间是被动接收方
- 同步和异步最大的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方
网络IO事件
- 客户端操作服务器时就会产生这三类事件(简称FD):writefds(写)、readfds(读)、和 exceptfds(异常)
IO多路复用机制
- I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符(File descriptor是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念),一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
缓存 I/O
- 缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O
- 在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间
- 缓存 I/O 的缺点是数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的
什么是用户空间与内核空间
- 操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限
- 为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间
进程切换
- 为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的
IO模型
分类
同步阻塞IO(Blocking IO)
- 指的是用户进程(或线程)主动发起,需要等待内核 IO 操作彻底完成后才返回到用户空间的 IO 操作
- 在 IO 操作过程中,发起 IO 请求的用户进程处于阻塞状态
- 用户进程调用recvfrom从内核读取数据,如果数据没准备好,一直阻塞直到数据准备好并且返回数据
同步非阻塞IO(Non-Blocking IO,NIO)
- 指的是用户进程主动发起,不需要等待内核 IO 操作彻底完成就能立即返回用户空间的 IO 操作
- 在 IO 操作过程中,发起 IO 请求的用户进程处于非阻塞状态
- 用户进程调用recvfrom从内核读取数据,如果数据还没准备好,返回EWOULDBLOCK(以轮询的方式读取);如果数据准备好了,返回数据
IO 多路复用(IO Multiplexing)
简述
- IO 多路复用(IO Multiplexing)属于一种经典的 Reactor 模式实现,有时也称为异步阻塞 IO
- IO 是指网络 IO,多路指多个TCP连接(即 socket 或者 channel),复用指复用一个或几个线程循环处理就绪的网络IO
- 实际上就解决了 NIO 中的频繁轮询 CPU 的问题,并且引入一种新的 select 系统调用
- 复用 IO 的基本思路就是通过 select 调用来监控多 fd(文件描述符),来达到不必为每个 fd 创建一个对应的监控线程的目的,从而减少线程资源创建的开销。一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核就能够将文件描述符的就绪状态返回给用户进程(或者线程),用户空间可以根据文件描述符的就绪状态进行相应的 IO 系统调用
- IO 多路复用的三种实现方式:select、poll、epoll
- 用户进程从内核空间读取数据,通过监听内核空间有数据就绪后的事件发生时,去查询就绪的数据,然后完成读取
实现方式
- select
- select 以阻塞的方式,监视FD事件列表,当有FD事件就绪时,就会唤醒线程,然后遍历FD事件列表,找到就绪的FD事件,并进行处理
- 遍历查找的时间复杂是O(N)。由于是采用轮询方式全盘扫描,会随着文件描述符 FD 数量增多而性能下降
- 最大连接数是 1024 个。单个进程打开的 FD 是有限制(通过FD_SETSIZE设置)
- 每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
- 几乎在所有的平台上支持,跨平台支持性好
- 数据结构:以数组的形式存储FD事件列表
- poll
- poll以死循环的方式,遍历FD事件列表(以链表的形式进行存储),每当发现有就绪的FD事件,就会执行处理逻辑
- 遍历查找的时间复杂是O(N)。由于是采用轮询方式全盘扫描,会随着文件描述符 FD 数量增多而性能下降
- 没有最大FD数限制(使用链表的方式存储 fd)
- 每次调用 poll(),都需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
- 几乎在所有的平台上支持,跨平台支持性好
- 数据结构:以链表的形式存储FD事件列表
- epoll
- Epoll哪个流发生了I/O事件会通知处理线程,对流的操作都是有意义的,时间复杂度降低到了O(1)
- 数据结构:以红黑树结构存储FD事件列表;以数组结构存储FD就绪事件列表
异步IO(Asynchronous IO,AIO)
- 指的是用户空间的线程变成被动接收者,而内核空间成为主动调用者
- 在异步 IO 模型中,当用户线程收到通知时,数据已经被内核读取完毕并放在用户缓冲区内,内核在 IO 完成后通知用户线程直接使用即可。而此处说的 AIO 通常是一种异步非阻塞 IO
- IO 完成后通知用户线程直接使用即可。而此处说的 AIO 通常是一种异步非阻塞 IO
信号驱动式I/O(Signal Driven IO,SIGIO)
- 当进程发起一个 IO 操作,会向内核注册一个信号处理函数,然后进程返回不阻塞
- 当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用 IO 读取数据
综合分析
- 同步IO的优点
- 同步流程对结果处理通常更为简单,可以就近处理
- 同步流程对结果的处理始终和前文保持在一个上下文内
- 同步流程可以很容易捕获、处理异常
- 同步流程是最天然的控制过程顺序执行的方式
- 异步IO的优点
- 异步流程可以立即给调用方返回初步的结果
- 异步流程可以延迟给调用方最终的结果数据,在此期间可以做更多额外的工作,例如结果记录等等
- 异步流程在执行的过程中,可以释放占用的线程等资源,避免阻塞,等到结果产生再重新获取线程处理
- 异步流程可以等多次调用的结果出来后,再统一返回一次结果集合,提高响应效率
- 异步IO使用场景
- 不涉及共享资源,或对共享资源只读,即非互斥操作
- 没有时序上的严格关系
- 不需要原子操作,或可以通过其他方式控制原子性
- 常用于IO操作等耗时操作,因为比较影响客户体验和使用性能5、不影响主线程逻辑
- 对比分析
- 同步的执行效率会比较低,耗费时间,但有利于我们对流程进行控制,避免很多不可掌控的意外情况
- 异步的执行效率高,节省时间,但是会占用更多的资源,也不利于我们对进程进行控制
IO编程
分类
传输方式维度
字节流
- InputStream
- OutputStream
字符流
- Reader
- Writer
字节流与字符流的区别
- 字节流读取单个字节,字符流读取单个字符(字符根据编码的不同,对应的字节也不同,如 UTF-8 编码中文汉字是 3 个字节,GBK编码中文汉字是 2 个字节)
- 字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)
数据操作维度
文件(File)
- FileInputStream
- FileOutputStream
- FileReader
- FileWrite
数组(Array)
- 字节
- ByteArrayInputStream
- ByteArrayOutputStream
- 字符
- CharArrayReader
- CharArrayWriter
管道操作(Piped)
- PipedInputStream
- PipedOutputStream
- PipedReader
- PipedWriter
数据流
- DataInputStream
- DataOutputStream
缓冲流(Buffered)
- BufferedInputStream
- BufferedOutputStream
- BufferedReader
- BufferedWriter
打印
- PrintStream
- PrintWriter
序列化、反序列化
- ObjectInputStream
- ObjectOutputStream
转换
- InputStreamReader
- OutputStreamWriter
常用类介绍
磁盘操作(File)
字节操作(InputStream 和 OutputStream)
字节操作(InputStream 和 OutputStream)
字符操作(Reader 和 Writer)
对象操作(Serializable)
- 序列化就是将一个对象转换成字节序列,方便存储和传输
- 不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态
- 序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常
- transient 关键字可以使一些属性不会被序列化
网络操作(Socket)
InetAddress
- 用于表示网络上的硬件资源,即 IP 地址
- 没有公有的构造函数,只能通过静态方法来创建实例
URL
- 统一资源定位符
- 可以直接从 URL 中读取字节流数据
Sockets
- 使用 TCP 协议实现网络通信
- ServerSocket: 服务器端类
- Socket: 客户端类
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出
Datagram
- 使用 UDP 协议实现网络通信
- DatagramSocket: 通信类
- DatagramPacket: 数据包类
通道
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写
- FileChannel
- 从文件中读写数据
- DatagramChannel
- 通过 UDP 读写网络中数据
- SocketChannel
- 通过 TCP 读写网络中数据
- ServerSocketChannel
- 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel
缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
名词说明
编码与解码
- 编码就是把字符转换为字节
- 解码是把字节重新组合成字符
零拷贝
Java零拷贝
- MappedByteBuffer
- MappedByteBuffer 是 NIO 基于内存映射(mmap),这种零拷贝方式的提供的一种实现,它继承自 ByteBuffer。FileChannel 定义了一个 map() 方法,它可以把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件
- DirectByteBuffer
Netty零拷贝
Netty 零拷贝完全是基于(Java 层面)用户态的,它的更多的是偏向于数据操作优化这样的概念
RocketMQ
mmap + write 这种零拷贝方式,适用于业务级消息这种小块文件的数据持久化和传输
Kafka
- Kafka 采用的是 sendfile 这种零拷贝方式,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输
- Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式