1️⃣ io原理
📝 同步和异步
名称 | 含义 |
---|---|
同步 | 从时间上强调处理事情的结果,强调结果意味着对结果的迫不及待,不管结果如何,反正你要立即给我一个结果响应,一直处于等待状态。 |
异步 | 调用者发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时调用者在等待结果过程中浪费时间是极其难受的,这个时候我们可以处理其他的请求,被调用者通常依靠事件、回调等机制来通知调用者其返回结果。 |
📝 阻塞和非阻塞
名称 | 含义 |
---|---|
阻塞 | 调用者发起一个请求,会一直等待请求结果返回,也就是当前线程会被挂起,没法做其他事情。 |
非阻塞 | 调用者发起一个请求,不用一直等待请求结果返回,可以去做其他的事情。 |
名称 | 含义 |
---|---|
同步阻塞 | 我到麻辣烫店点好了东西,然后一直在那等着做好,偶尔还要问下:我的弄好了没。 |
同步非阻塞 | 我点好东西后,买水果或者买凉菜去了,过了会回来问下:我的弄好了没。 |
异步阻塞 | 我是在买水果还是在买凉菜的时候,人家老板打电话过来了,说你的麻辣烫已经做好了,你过来拿就好了。 |
异步非阻塞 | 老板打电话不仅没让我过去拿,还说亲自给我送过来,让我安心的先买水果。 |
📝 io原理
操作系统与Java基于流的I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在的协助下完成的。I/O类喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据,java.io的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io类则喜欢一铲子一铲子地加工数据。
名称 | 含义 |
---|---|
用户态 | 对于操作系统而言,JVM只是一个用户进程(应用程序),处于用户态空间中,而处于用户态空间的进程是不能直接操作底层的硬件(磁盘/网卡)。 |
系统调用 | 区别于用户进程调用,系统调用时操作系统级别的api,比如java IO的读取数据过程(使用缓冲区),用户程序发起读操作,导致“ syscall read ”系统调用,就会把数据搬入到 一个buffer中;用户发起写操作,导致 “syscall write ”系统调用,将会把一个 buffer 中的数据 搬出去(发送到网络中 or 写入到磁盘文件) |
内核态 | 用户态的进程要访问磁盘/网卡(也就是操作IO),必须通过系统调用,从用户态切换到内核态(中断、trap),才能完成。 |
局部性原理 | 操作系统在访问磁盘时,由于局部性原理,操作系统不会每次只读取一个字节(代价太大),而是借助硬件直接存储器存取(DMA)一次性读取一片(一个或者若干个磁盘块)数据。因此,就需要有一个“中间缓冲区”–即内核缓冲区。先把数据从磁盘读到内核缓冲区中,然后再把数据从内核缓冲区搬到用户缓冲区 |
从进程看,分用户态和内核态 | 应用程序属于用户态,系统调用则内核态 |
从内存看,分用户内存和内核内存 | 用户内存 = 应用内存 = JVM内存 = User space 内核内存 = 系统内存 = Kernel space |
2️⃣ io模型
名称 | 含义 |
---|---|
阻塞IO | 用户进程阻塞,知道io就绪且io处理(读写)完成,阻塞才解除。 |
非阻塞IO | 用户进程不阻塞,需要不断轮询io是否就绪,很占用cpu资源,所以一般不用 |
多路复用IO | 用户进程阻塞,也是轮询io是否就绪,但是和非阻塞IO不一样的是,轮询io的是系统进程而非应用进程,实际使用了代理(select/poll/epoll),可以避免cpu空转,所以效率较高,而且1个进程可以管理多个socket,java nio使用的就是多路复用模型。 |
信号驱动式 I/O | (有点类似回调)用户进程不阻塞,而是安装一个信号处理函数,当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。 |
异步IO | 用户进程不阻塞,发起io请求(读写)后就可以做自己的事了,io就绪等待和io操作交给操作系统去完成,完成后调用用户进程处理函数,所以整个过程都是异步的。 |
多路复用IO和非阻塞比较 | 非阻塞是用户进程去轮询io是否就绪,所以用户进程是不阻塞的。 而多路复用,用户进程是阻塞的,轮询io是否就绪是系统进程(内核)在进行。 多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过 户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用线程要高的多。 |
📝 同步IO
📝 阻塞IO
📝 非阻塞IO
📝 异步IO
3️⃣ io分类
名称 | 含义 |
---|---|
BIO | 同步阻塞I/O。一个连接一个线程,线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成。 |
NIO | 同步非阻塞I/O。一个请求一个线程。服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。 |
AIO | 异步非阻塞I/O(注:JDK1.7升级了NIO类库,升级后的NIO类库被称为NIO2.0也就是AIO)。一个有效请求一个线程。用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。 |
📝 BIO
BIO通信(一请求一应答)
📝 NIO
这里需要了解3个概念:缓冲区、通道、多路复用器
名称 | 含义 |
---|---|
缓冲区 | 即Buffer,Buffer是一个对象,它包含一些要写入或者要读出的数据,在NIO库中,所有数据读写操作都是用缓冲区处理的。具体一些实现就不说了,通常它是一个字节数组(ByteBuffer)。 |
通道 | 即Channel,通过它读取和写入数据,就像电话线一样。这里大家是不是有想到流(Stream),区别在于Stream是单向,而Channel是双向的。主要实现呢有以下:FileChannel、DatagramChannel、SocketChannel,通过英文名是不是可以看出个大概意思:文件IO、UDP和TCP |
多路复用器 | 即Selector,说白了是用来管理Channel的,Channel注册在Selector中,如果某个Channel上面有TCP连接接入,会被Selector轮询出来,通过相关处理进行后续的I/O操作 |
通信步骤
名称 | 含义 |
---|---|
服务端通信步骤 | 1.创建ServerSocketChannel实例、并且绑定端口; 2.创建selector实例; 3.将ServerSocketChannel实例注册到选择器上,并监听accept事件; 4.有请求(accept)进来获取客户端的socketChannel的连接,将连接实例注册到选择器上; 5.IO操作(进入缓冲区Buffer); 6.关闭ServerSocketChannel实例 |
客户端通信步骤 | 1.创建SocketChannel实例和Selector选择器; 2.连接服务器; 3.将SocketChannel注册到selector选择器; 4.发送数据; 5.关闭SocketChannel; |
📝 AIO
真正实现了异步IO, 基于windows上IOCP和Linux系统上Epoll 机制实现。底层实现是由操作系统完成的,与NIO不同,当进行读写操作时,只需直接调用AIO.read()或AIO.write()方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
📝 适用场景
名称 | 含义 |
---|---|
BIO | 适用于连接数目比较小,并且一次发送大量数据的场景; |
NIO | 适用于连接数目多,连接比较短,常用于聊天服务器开发工作(榜上有名的Netty是很大的实践); |
AIO | 适用于连接数目多,连接比较长。这个目前市面上应用还不是很广泛 |
4️⃣ Java IO(输入输出)介绍
- IO是Input(输入)和Output(输出)的首字母缩写。
- I(输入Input):指向Java程序中输入数据,即Java程序从外部获取数据。
- O(输出Output):指的是Java程序向外部输出数据,即Java程序向外部发送数据。
流:在Java程序和外部之间,数据像水流一样按照顺序传输。Java中,流有两种形式,字节流和字符流,下文会讲解。
外部(也就是数据源)包括:源设备 和 目标设备。
源设备:Java程序使用Input(输入)获取数据的来源。
目标设备:Java程序使用Output(输出)发送数据的目的地
Java IO用在Java程序和外部进行数据交互,Java程序运行在内存中,
要与外部(如:磁盘、网络、数据库等)地方交互数据则需要使用Java IO。
5️⃣ 四大IO抽象类
- 字节流IO抽象类:InputStream和OutputStream,数据单位为1字节(8Bit)
- InputStream:字节输入流
常用方法(2个):
int read(); 读取一个字节的数据到程序中,数据类型是int(0~255之间的一个数),
如果没有读到数据会返回-1(输入完毕)。如果读到的是字符则返回对应的ASCII码。
void close(); 关闭输入流对象,释放相关资源。
- OutputStream:字节输出流
常用方法(2个):
void write( int n ); 向目的地写入一个字节 int(0~255之间的一个数),没有返回值。
void close(); 关闭输出流对象,释放相关资源。
- 字符流IO抽象类:Reader和Writer,数据单位为字符(编码是Unicode,2或4字节)
- Reader:字符输入流
常用方法(2个):
int read(); 读取一个字符的数据到程序中,数据类型是int(0~65535之间的一个数,即Unicode值),
如果没有读到数据会返回-1(输入完毕)。
void close(); 关闭输入流对象,释放相关资源。
- Writer:字符输出流
常用方法(2个):
void write(int n); 向目的地写入一个字节 int(0~65535之间的一个数),没有返回值。
void close(); 关闭输出流对象,释放相关资源。
6️⃣ Java IO体系
Java将四大抽象类的子类们分为了:节点流和处理流,两大类
节点流:直接和数据源相连。
处理流:包装在节点流之上,增加缓存提高效率,提供方法大批量输入输出数据。
名称 | 含义 | 类型 |
---|---|---|
File | 非流式IO 用来在Java内存中表示一个文件或目录 | 非流式 |
RandomAcessFile | 随机访问流 只能对文件进行随机读写操作的访问 | 非流式 |
FileInputStream FileOutputStream | 节点流,以字节为单位,直接操作文件。 | 节点流 |
BufferedInputStream BufferedOutputStream | 处理流,将InputStream/OutputStream对象进行包装,增加缓存功能,提高读写效率 | 处理流 |
ByteArrayInputStream ByteArrayOutputStream | 节点流,以字节为单位直接操作 字节数组对象 | 节点流 |
ObjectInputStream ObjectOutputStream | 处理流,以字节为单位直接操作 对象 | 处理流 |
DataInputStream DataOutputStream | 处理流,以字节为单位直接操作“基本数据类型与字符串类型 | 处理流 |
InputStreamReader OutputStreamWriter | 处理流,将字节流对象转换为字符流对象 | 处理流 |
PrintStream | 处理流,将OutputStream进行包装,可以方便的输出字符,更加灵活 | 处理流 |
FileReader FileWriter | 节点流,以字节为单位直接对 文本文件 进行读写 | 节点流 |
BufferedReader BufferedWriter | 处理流,将Reader/Writer对象进行包装,增加缓存功能,提高读写效率 | 处理流 |
7️⃣ 字节流和字符流
📝 字节流
Java 中的字节流处理的最基本单位为 1 个字节,通常用来处理二进制数据。
字节流类 InputStream 和 OutputStream 类均为抽象类,代表了基本的输入字节流和输出字节流。
📝 字符流
Java 中的字符流处理的最基本的单元是 Unicode 代码单元(大小2字节),通常用来处理文本数据。
📝 区别
- 字节流操作的基本单元是字节;字符流操作的基本单元是字符
- 字节流默认不使用缓冲区;字符流使用缓冲区
- 字节流通常用于处理二进制数据,不支持直接读写字符;字符流通常用于处理文本数据
- 在读写文件需要对文本内容进行处理:按行处理、比较特定字符的时候一般会选择字符流;仅仅读写文件,不处理内容,一般选择字节流
- 我们在使用IO流时通常会选择缓冲流,因为它具有更强的读写能力,可以减少资源浪费和节约时间;但是使用BufferedReader和BufferedWriter时必须将字节流转换为字符流。这也是我们经常会同时使用字节流、字符流和缓冲流的原因。