1.什么是IO流?
IO是input输入和output输出都组成,一个应用程序的输入往往是另一个应用程序的输出。
1.1 流
在JavaIO中,流是一个很核心的概念。从概念上来说,是一个连续的数据流,你既可以从流中读取数据,也可以往流中写数据。在Java中有字节流(以字节为单位进行读写),字符流(以字符为单位进行读写)。
下图描绘了一个程序从数据源读取数据,然后将数据输出到其他媒介的原理:
从程序的角度来说,使用InputStream/Reader去源数据读取,再通过OutputStream/Writer将数据输出为结果,如下图:
1.2 JavaIO流
JavaIO流既是可以读取,也可以写入的数据流。流与数组是有区别的,流无法通过索引去读写数据,所以不能像数组那样前后移动读取数据,除非使用RandomAccessFile 处理文件。流仅仅只是个连续的数据流。
RandomAccessFile是Java提供的功能最丰富的文件内容访问类,它拥有了众多的方法去访问文件内容。与普通的输入/输出流不同的是,它可以支持随机访问的方式,即程序可以通过跳转的文件都任意地方来读写数据。
下面是IO流的类图关系:
1.2.1 字符流与字节流的区别
上面讲过JavaIO流共分为两大类,字符流与字节流,他们之间的有一定的区别:
1.定义不同:字节流能处理所有类型的数据(如图片,音视频等),而字符流只能处理字符类型的数据。
2.读写单位不同:字节流以字节(每字节8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
3.处理方式不同:字节流主要由InputStream和OutputStream作为基类,字符流主要由Reader和Writer作为基类。
4.缓冲区不同:字节流读取的数据不经过缓存区,而字符流经过。
5.编码方式不同:字节流默认不适用缓冲区,字符流使用缓冲区。字节流采用ASCII编码,而字符流使用unicode编码。
6.性能不同:字节流在处理大量数据时具有较高的速度和效率,字符流在处理文本数据时具有较好的可读性和可操作性,但速度可能较慢。
7.内存占用不同:字节流每个字节占用一定内存,字符流处理文本数据时可以减少内存占用。
8.可操作性不同:字节流可以直接操作字节,精确控制数据传输和处理,字符流需要进行额外的编码或解码操作。
1.2.2 常用使用方式
用字节流去操作文本文件
public static void main(String[] args){
//1.通过FileOutputStream去写入文件,如果文件不存在会自动创建
FileOutputStream fos = null;
try{
fos = new FileOutputStream("文件路径");
fos.write("hello".getBytes());
}catch (Exception e){
e.printStackTrace();
}finally {
try{
if(fos != null){
fos.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
//2.通过FileInputStream去读取文件
FileInputStream fis = null;
try{
fis = new FileInputStream("文件路径");
byte[] bytes = new byte[1024];
int len = 0;
while((len = fis.read(bytes)) != -1){
System.out.println(new String(bytes,0,len));
}
}catch (Exception e){
e.printStackTrace();
}finally {
try{
if(fis != null){
fis.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
运行结果:
注意:java中的大部分接触底层资源的输入或者输出流在使用完之后都必须要关闭,而关闭的方式可以手动调用IOUtils.close() / Closeable.close()来手动关闭流,或者使用的try-catch- finallly语句块来显式的关闭流资源。
java中的输出流在使用完之后都必须使用从flushable接口中实现的flush方法来实现对流的刷新,所有的流都实现了java.io.Flushable接口,这意味着所有的输出流都是可以刷新的,都有flush方法,养成一个好习惯,输出流在最终输出之后,一定要记得flush方法刷新一下,这个刷新表示将通道或者管道中的剩余的未输出的数据强行输出成功,也就是清空管道,刷新的作用的作用就是清空管道,如果没有使用flush方法去清空输出流管道,就会导致数据丢失。
2.UNIX IO模型
2.1 IO模型分类
1.同步阻塞IO
2.同步非阻塞IO
3.IO多路复用
4.信号驱动IO
5.异步IO
可以从两个维度去理解上述模型:
1.区分同步和异步。同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件,回调等机制来实现任务间次序关系。
2.区分阻塞和非阻塞。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新链接建立完毕,或者数据读取,写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,其他操作在后台继续处理。
不能一概而论去认为同步或阻塞是低效,具体还需要看应用的综合特征。
当发起一次IO操作时,需要经历两个步骤:
1.IO调用:应用程序进程向操作系统内核发起调用
2.IO执行:操作系统内核完成IO操作
上述模型之间的区别就是在于这两个步骤的处理方式不同
2.2 同步阻塞IO
用户线程发起调用后就阻塞了,让出CPU,内核等待数据的到来,接着把数据拷贝到用户空间,再把用户线程叫醒。
2.3 同步非阻塞IO
同步非阻塞不会像同步阻塞那样两个阶段都阻塞,它在第一个阶段中向内核请求数据时如果此时内核还没准备好数据,那么它会直接返回一个错误,不会一直等待下次。等待一定时间后会继续发起IO请求。
2.4 IO多路复用
用户线程的读取操作分成两步了,线程先发起 select 调用,目的是问内核数据准备好了吗?等内核把数据准备好了,用户线程再发起 read 调用。在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的。那为什么叫 I/O 多路复用呢?因为一次 select 调用可以向内核查多个数据通道(Channel)的状态,所以叫多路复用。
2.5 信号驱动IO
首先开启 Socket 的信号驱动 I/O 功能,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。信号驱动式 I/O 模型的优点是我们在数据报到达期间进程不会被阻塞,我们只要等待信号处理函数的通知即可
2.6 异步IO
用户线程发起 read 调用的同时注册一个回调函数,read 立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。
3. Java IO模型
3.1 BIO
BIO(blocking IO)即阻塞IO。指的是传统的java.io包,基于流模型实现。
java.io包提供了一些基础的IO功能,比如说文件抽象,输入输出流等,交互方式大多是同步,阻塞,在读取/写入时,线程会一直阻塞在那里,他们之间的调用时可靠的线性顺序。BIO的优点就是代码简单,直观;缺点就是IO效率和扩展性存在局限性,容易成为应用性能的瓶颈。
由于BIO会阻塞,所以它并不适合高并发的场景。使用BIO的方式通常是由一个独立的Acceptor接收器线程去监听客户端连接,服务端一般在一个while(true)的循环中调用accept()方法等待客户端的连接请求,一旦接收到一个连接请求,就可以建立起Socket,并基于这个Socket进行读写操作。此时不会再接收其他的客户端连接请求,必须等待当前连接的操作完成。
3.2 NIO
NIO是一种同步非阻塞的IO模型,其中,N可以理解为Non-blocking,不仅仅是New。它支持面向缓冲,基于通道的IO操作方法,与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞模式,阻塞模式使用就像传统的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载,低并发的应用,可以使用同步阻塞IO来提升开发效率和维护,对于高负载,高并发的应用,应使用NIO的非阻塞模式开发。
BIO是阻塞的,NIO是非阻塞的。
BIO 的各种流是阻塞的。这意味着,当一个线程读写 时,该线程会被阻塞,直到有一些数据被读取,或数据完全写入。在此期间,该线程不能再干其他任何事。
NIO 使我们可以进行非阻塞 IO 操作。比如说,单线程中从通道读取数据到缓冲区(buffer),同时可以继续做别的事情,当数据读取到缓冲区(buffer)中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。