IO基础
什么是流?作用是什么?
- 流是一种有顺序的,有起点和终点的字节集合,是对数据传输的总成或抽象。即数据在两设备之间的传输称之为流,流的本质是数据传输,根据数据传输的特性讲流抽象为各种类,方便更直观的进行数据操作。
字符流和字节流的区别是什么?
-
字符流的由来:因为数据编码的不同,而有了对字符进行高效操作的流对象,其本质就是基于字节流读取时,去查了指定的码表。
-
字符流和字节流的区别:
-
读写单位不同:字节流一字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
-
处理对象不同:字节流能处理所有类型的数据,而字符流只能处理字符类型的数据。
-
字节流操作的时候本身是不会用到缓冲区的,是对文件本身的直接操作。而字符流在操作的时候是会用到缓冲区的,通过缓冲区来操作文件。
-
-
结论:优先使用字节流,首先因为在硬盘上所有的文件都是以字节的形式进行传输或保存的,包括图片等内容。但是字符流只是在内存中才会形成,所以在开发中字节流使用广泛。
读写原始数据,采用什么流?
- InputStream/OutputStream
为了提高读写性能,采用什么流?
- BufferedInputStream/BufferedOutputStream
对各种基本数据类型和String类型的读写,采用什么流?
- DataInputStream/DataOutputStream
指定字符编码,采用什么流?
- InputStreamReader/OutputStreamWriter
Linux常见IO模型有哪些?
-
linux下有五种常见的IO模型:
1、阻塞 I/O(blocking IO)
2、非阻塞 I/O(nonblocking IO)
3、I/O 多路复用( IO multiplexing)
4、信号驱动 I/O( signal driven IO)
5、异步 I/O(asynchronous IO)
只有⑤是异步模型,其余皆为同步模型
-
阻塞IO模型: 阻塞IO模型是最常见的IO模型了,对于所有的“慢速设备”(socket、pipe、fifo、terminal)的IO默认的方式都是阻塞的方式。阻塞就是进程放弃cpu,让给其他进程使用cpu。进程阻塞最显著的表现就是进程睡眠了。阻塞的时间通常取决于数据是否到来。 这种方式使用简单,但随之而来的问题就是会形成阻塞,需要独立线程配合,而这些线程在大多数时候都是没有进行运算的。Java的BIO使用这种方式,问题带来的问题很明显,一个Socket需要一个独立的线程,因此,会造成线程膨胀
-
非阻塞IO模型: 非阻塞IO就是设置IO相关的系统调用为non-blocking,随后进行的IO操作无论有没有可用数据都会立即返回,并设置errno为EWOULDBLOCK或者EAGAIN。我们可以通过主动check的方式(polling,轮询)确保IO有效时,随之进行相关的IO操作。当然这种方式看起来就似乎不太靠谱,浪费了太多的CPU时间
-
多路复用IO模型: 为了解决阻塞I/O的问题,就有了I/O多路复用模型,多路复用就是用单独的线程(是内核级的, 可以认为是高效的优化的) 来统一等待所有的socket上的数据, 一当某个socket上有数据后, 就启用用户线程(可能是从线程池中取出, 而不是重新生成), copy socket data, 并且处理message.因为网络延迟的原因, 同时在处理socket data的用户线程往往比实际的socket数量要少很多. 所以实际应用中, 大部分是用线程池, 池中thread数量可随socket的高峰和低谷 而动态调整多路复用I/O中内核中统一的wait socket data那部分可以理解成是非阻塞, 也可以理解成阻塞. 可以理解成非阻塞 是因为它不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户线程来处理, 理解成阻塞, 是因为它和用户空间(Appliction)层的非阻塞socket的不同是: socket中没有数据时, 内核还是wait(阻塞)的, 而用户空间的非阻塞socket没有数据也会返回, 会造成CPU的浪费Linux下的select和poll 就是多路复用模式,poll相对select,没有了句柄数的限制,但他们都是在内核层通过轮询socket句柄的方式来实现的, 没有利用更底层的notify机制. 但就算是这样,相对阻塞socket也已经进步了很多很多了! 毕竟用一个内核线程就解决了,阻塞socket中N多线程都在无谓地wait的局面多路复用I/O 还是让用户层来copy socket data. 这个过程是将内核中的socket buffer copy到用户空间的 buffer. 这有两个问题: 一是多了一次内核空间switch到用户空间的过程, 二是用户空间层不便暴露很低层但很高效的copy方式(比如DMA), 所以如果由内核层来做这个动作, 可以更好地提高效率
-
信号驱动IO模型: 所谓信号驱动,就是利用信号机制,安装信号SIGIO的处理函数(进行IO相关操作),通过监控文件描述符,当其就绪时,通知目标进程进行IO操作(signal handler)
-
异步IO模型: 由于异步IO请求只是写入了缓存,从缓存到硬盘是否成功不可知,因此异步IO相当于把一个IO拆成了两部分,一是发起请求,二是获取处理结果。因此,对应用来说增加了复杂性。但是异步IO的性能是所有很好的,而且异步的思想贯穿了IT系统方方面面
-
select、poll、epoll的区别是什么?
- 支持一个进程所能打开的最大连接数
- select 是 32位机默认是1024个,64位机默认是2048
- poll 本质上于select 没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
- epoll 虽然有连接数上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的可以打开20万左右的连接。
- fd剧增后带来的I/O效率问题
- select 每次调用事都会对连接进行线性遍历,所以随着fd的增加会造成遍历速度慢,呈线性下降性能问题。
- poll 同上
- epoll epoll内核中实现是根据每个fd的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃的socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有的socket都很活跃的情况下,可能会有性能问题
- 消息传递方式
- select 内核需要将消息传递到用户空间,都需要内核拷贝动作
- poll 同上
- epoll 通过内核和用户空间共享一块内存来实现的
- 支持一个进程所能打开的最大连接数
BIO、NIO、AIO 的区别是什么?**
含义方面:
- BIO(Blocking IO)是同步并阻塞的 IO,线程发起 IO 请求后,不论内核是否准备好 IO 操作,都会一直阻塞直到操作完成;
- NIO(Non-blocking IO)是同步非阻塞的 IO,线程发起 IO 请求后立即返回;内核在做好 IO 操作的准备之后,通过调用注册的回调函数通知线程做 IO 操作,线程开始阻塞,直到操作完成;
- AIO(Asynchronous IO)是异步非阻塞的 IO,线程发起 IO 请求后立即返回;内存做好 IO 操作的准备之后,做 IO 操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做 IO 操作完成或者失败。
应用场景方面:
- BIO 从 JDK1.4 之前的版本,适用于低负载、低并发、业务逻辑耗时较长的场景
- NIO 从 JDK1.4 开始支持,适用于高负载高并发且业务逻辑简单(轻操作)的场景,典型场景是聊天服务器
- AIO 从 JDK1.7 开始支持,适用于高负载高并发且业务逻辑复杂(重操作)的场景,典型场景是相册服务器
发展过程:
BIO->NIO->AIO
-
BIO:在服务端,通常是在 while 循环中调用 accept 方法等待接收客户端的连接请求,一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成。 如果 BIO 要能够同时处理多个客户端请求,就必须使用多线程,即每次 accept 阻塞等待来自客户端请求,一旦受到连接请求就建立通信套接字同时开启一个新的线程来处理这个套接字的数据读写请求,然后立刻又继续 accept 等待其他客户端连接请求,即为每一个客户端连接请求都创建一个线程来单独处理;
-
NIO :与 BIO 的最大的区别是多路复用的思想,只需要开启一个线程就(或者少量多线程)可以处理来自多个客户端的 IO 事件,用来扩展 BIO 的高并发场景。
- 若服务端监听到客户端连接请求,便为其建立通信套接字 (java 中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。
- 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。
-
AIO:与 NIO 不同,当进行读写操作时,只须直接调用 API 的 read 或 write 方法即可。这两种方法均为异步的,完成后会主动调用回调函数。如果是读操作,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序;如果是写操作,操作系统在将 write 方法传递的流写入完成后,也会通知应用程序。 在 JDK1.7 中,主要在 java.nio.channels 包下增加了下面四个异步通道来实现:
-
AsynchronousSocketChannel
-
AsynchronousServerSocketChannel
-
AsynchronousFileChannel
-
AsynchronousDatagramChannel
这四个类的 read/write 方法,都会返回一个带回调函数的对象,当执行完读取 / 写入操作后,直接调用回调函数。
-
字节流、字符流的区别及适用场景分别是什么?
区别:
- 处理单元不同,字节流处理的最基本单位为 1 个字节,字符流处理的最基本的单元是 Unicode 代码单元(大小 为2 字节)
- 字节流默认不使用缓冲区;字符流使用缓冲区
使用场景:
- 字节流实际上可以处理任何文件,因为字节是存储的基础单元;而待处理的流如果是可打印的字符,那么用字符流更方便一些
JDK 中, 字节流操作一般都是 InputStream, OutputStream 以及各种包装类;而字符流字符操作一般使用 Writer,Reader 等
网络 I/O 优化
网络 I/O 优化通常有一些基本处理原则:
- 一个是减少网络交互的次数:要减少网络交互的次数通常我们在需要网络交互的两端会设置缓存,比如 Oracle 的 JDBC 驱动程序,就提供了对查询的 SQL 结果的缓存,在客户端和数据库端都有,可以有效的减少对数据库的访问。关于 Oracle JDBC 的内存管理可以参考《 Oracle JDBC 内存管理》。除了设置缓存还有一个办法是,合并访问请求:如在查询数据库时,我们要查 10 个 id,我可以每次查一个 id,也可以一次查 10 个 id。再比如在访问一个页面时通过会有多个 js 或 css 的文件,我们可以将多个 js 文件合并在一个 HTTP 链接中,每个文件用逗号隔开,然后发送到后端 Web 服务器根据这个 URL 链接,再拆分出各个文件,然后打包再一并发回给前端浏览器。这些都是常用的减少网络 I/O 的办法。
- 减少网络传输数据量的大小:减少网络数据量的办法通常是将数据压缩后再传输,如 HTTP 请求中,通常 Web 服务器将请求的 Web 页面 gzip 压缩后在传输给浏览器。还有就是通过设计简单的协议,尽量通过读取协议头来获取有用的价值信息。比如在代理程序设计时,有 4 层代理和 7 层代理都是来尽量避免要读取整个通信数据来取得需要的信息。
- 尽量减少编码:通常在网络 I/O 中数据传输都是以字节形式的,也就是通常要序列化。但是我们发送要传输的数据都是字符形式的,从字符到字节必须编码。但是这个编码过程是比较耗时的,所以在要经过网络 I/O 传输时,尽量直接以字节形式发送。也就是尽量提前将字符转化为字节,或者减少字符到字节的转化过程。
- 根据应用场景设计合适的交互方式:所谓的交互场景主要包括同步与异步阻塞与非阻塞方式,下面将详细介绍。
同步与异步
-
所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。
-
在设计到 IO 处理时通常都会遇到一个是同步还是异步的处理方式的选择问题。因为同步与异步的 I/O 处理方式对调用者的影响很大,在数据库产品中都会遇到这个问题。因为 I/O 操作通常是一个非常耗时的操作,在一个任务序列中 I/O 通常都是性能瓶颈。但是同步与异步的处理方式对程序的可靠性影响非常大,同步能够保证程序的可靠性,而异步可以提升程序的性能,必须在可靠性和性能之间做个平衡,没有完美的解决办法。
阻塞与非阻塞
- 阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。
两种的方式的组合
组合的方式可以由四种,分别是:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞,这四种方式都对 I/O 性能有影响
- 同步阻塞
最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态 - 同步非阻塞
提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。 这种方式通常能提升 I/O 性能,但是会增加 CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗,也就是系统的瓶颈是在 I/O 还是在 CPU 上 - 异步阻塞
这种方式在分布式数据库中经常用到,例如在网一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其它机器上,这些备份记录通常都是采用异步阻塞的方式写 I/O。 异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况 - 异步非阻塞
这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 I/O 组合方式。如 Cassandra 的 Gossip 通信机制就是采用异步非阻塞的方式。 它适合同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高