什么是IO流

流代表任何有能力产出数据的数据流对象,或者有能力接收数据的接收端对象《Think in Java》

:流是一个抽象的概念。当Java程序需要从数据源读取数据时,会开启一个到数据源的流。同样,当程序需要输出数据到目的地时也一样会开启一个流,我们可以认为数据就是在“流”上传输的流的创建是为了更方便地处理数据的输入输出,流的目的地可以是文件,内存和网络

流的本质数据传输

流的特性:

  • 先进先出:最先写入输出流的数据最先被输入流读取到
  • 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。
  • 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流

作用:为数据源和目的地建立一个输送通道

IO流

IO:即in和out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。Java 中是通过流处理IO 的
**

以一次文件读取为例,我们需要将磁盘上的数据读取到用户空间,那么这次数据转移操作其实就是一次I/O操作,更具体的说是一次文件I/O。我们浏览网页,其中在请求一个网页时,服务器通过网络把数据发送给我们,此时程序将数据从TCP缓冲区复制到用户空间,那么这次数据转移操作其实也是一次I/O操作,更具体的说是一次网络I/O

简单来说,IO流就是Java用来处理IO操作的流。关于IO的类都存放在java.io包下

java IO读写的原理

无论是Socket的读写还是文件的读写,在Java层面的应用开发或者是linux系统底层开发,都属于输入input和输出output的处理,简称为IO读写。在原理上和处理流程上,都是一致的。区别在于参数的不同。

用户程序进行IO的读写,基本上会用到read&write两大系统调用。可能不同操作系统,名称不完全一样,但是功能是一样的

注意:read系统调用,并不是把数据直接从物理设备,读数据到内存。write系统调用,也不是直接把数据,写入到物理设备。

read系统调用,是把数据从
内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区**。这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。

底层的读写交换,是由操作系统kernel内核完成的

进程缓冲区和内核缓冲区

缓冲区的目的,是为了减少频繁的系统IO调用

系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。

有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区(也叫用户缓冲区),write把数据从进程缓冲区复制到内核缓冲区中

等待缓冲区达到一定数量的时候,再进行IO的调用,提升性能。至于什么时候读取和存储则由内核来决定,用户程序不需要关心。

在linux系统中,系统内核也有个缓冲区叫做内核缓冲区。每个进程有自己独立的缓冲区,叫做进程缓冲区
所以,用户程序的IO读写程序,大多数情况下,**并没有进行实际的IO操作,而是在进程缓冲区和内核缓冲区之间进行数据的交换**

读写过程

比如一次文件读写的过程
(1)进程调用read系统,申请从内核缓冲区读取数据
(1)内核缓冲区调用read系统从磁盘读取文件数据
(2)进程将数据从内核缓冲区拷贝到用户缓冲区(进程缓冲区)
(3)进程对用户缓冲区内容数据进行读写
(4)进程调用write系统将用户缓冲区内容写入内核缓冲区
(5)当IO操作达到一定的量时,将数据从内核缓冲区写入磁盘

Java IO流 - 图1

IO模型

服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种

(1)同步阻塞IO(Blocking IO,BIO)

同步: 数据就绪后,仍要用户代码主动调用读取系统调用接口才会将数据读到用户空间。

异步: 调用异步io系统接口后,用户代码直接返回无需等待 ,操作系统会轮询设备,然后再数据准备好时自动将数据复制到用户内存,然后通知用户代码数据已就绪。

阻塞IO(blocking IO):发出io请求时,系统调用不直接返回,由操作系统负责轮询设备,直到数据准备好后,再让用户代码继续执行。

非阻塞IO(non-blocking IO):发出io请求时,若不能完成操作,则直接返回用户代码,并给出对应的错误码。用户代码负责轮询,即不断尝试读取并判断是否读取失败,直到读取成功

Java IO流 - 图2

jaav的socket就是默认同步阻塞的IO。

特点:在内核进行IO执行的两个阶段,用户线程都被阻塞了

优点:程序简单,在阻塞等待数据期间,用户线程挂起。用户线程基本不会占用 CPU 资源。

缺点:一般情况下,会为每个连接配套一条独立的线程,或者说一条线程维护一个连接成功的IO流的读写。在并发量小的情况下,这个没有什么问题。但是,当在高并发的场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上,BIO模型在高并发场景下是不可用的

(2)同步非阻塞IO(Non-blocking IO,NIO)

非阻塞IO,指的是内核立即返回给用户一个状态值,**用户空间无需等到内核的IO操作彻底完成**,可以立即返回用户空间,执行用户的操作,处于非阻塞的状态。

Java IO流 - 图3
特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止

优点:每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好

缺点:需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。

(3)IO多路复用(IO Multiplexing)

内核将数据从外设读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区这两个阶段在IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。

在Linux系统中,对应的系统调用为select/epoll系统调用。通过该系统调用,**一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用**,用户程序都不需要阻塞。

基本原理:**
(1)进行select/epoll系统调用(当用户进程调用了select,那么整个线程会被阻塞掉),查询可以读的连接。kernel会查询所有select的可查询socket列表,当任何一个socket中的数据准备好了,select就会返回。

(2)用户线程获得了目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。

(3)用户线程解除阻塞状态,用户线程终于真正读取到数据,继续执行。

Java IO流 - 图4
**
IO多路复用模型的基本原理就是select/epoll系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。因此,好处也就显而易见了——通过一次select/epoll系统调用,就查询到到可以读写的一个甚至是成百上千的网络连接。

特点:IO多路复用模型的IO涉及两种系统调用(System Call),另一种是select/epoll(就绪查询),一种是IO操作。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。

和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。

优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。

Java语言的NIO(New IO)技术,使用的就是IO多路复用模型。在Linux系统上,使用的是epoll系统调用。

缺点:本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。

(4)异步IO(Asynchronous IO,AIO)

异步IO模型是比较理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。另一方面,内核会等待数据准备完成,然后将数据复制到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就是说用户线程完全不需要关心实际的整个IO操作了,只需要发起请求就行了,当收到内核的成功信号时就可以直接去使用数据了

那实际上,这种方式后半段的操作将用户空间的线程变成被动接受者,而内核空间成主动调用者,用户空间的线程需要向内核空间注册各种IO事件的回调函数,由内核去主动调用。

Java IO流 - 图5

特点:在内核等待数据和复制数据的两个阶段,用户线程都不是block(阻塞)的
缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持

在JDK1.4之前,基于Java的所有socket通信都是使用阻塞I/O(BIO),JDK1.4提供了了非阻塞I/O(NIO)功能,不过虽然名字叫做NIO,实际底层模型是I/O多路复用,JDK1.7提供了针对异步I/O(AIO)功能

IO流分类

Java的IO模型设计非常优秀,它使用Decorator(装饰者)模式,按功能划分Stream,允许开发动态装配组合这些Stream来完成功能。那么Java中都有哪些IO流呢?

数据流向-输入流&输出流

按照数据的流动方向,可以分为输入流(InputStream/Reader)和输出流(OutputStream/Writer)**,**此输入、输出是相对于我们写的代码程序而言,

输入流(InputStream/Reader):从别的地方(本地文件,网络上的资源等)获取资源 输入到 我们的程序中
  
输出流(OutputStream/Writer):从我们的程序中 输出到 别的地方(本地文件), 将一个字符串保存到本地文件中,就需要使用输出流。

操作单元-字节流&字符流

根据数据不同的操作单元,分为字节流(InputStream/OutputStream)和字符流(Reader/Writer)

1字符 = 2字节 、 1字节(byte) = 8位(bit) 、 一个汉字占两个字节长度,即字节<字符

字节流(InputStream/OutputStream):每次读取(写出)一个字节,当传输的资源文件有中文时,就会出现乱码,所有字节流对象都实现了InputStreamOutputStream抽象类
字符流(Reader/Writer):每次读取(写出)两个字节,有中文时,使用该流就可以正确传输显示中文。所有字符流对象都实现了ReaderWriter抽象类

可以说,为了更方便地处理中文字符,Java才推出了字符流。

字节流和字符流的其他区别:

  • 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。

  • 字节流本身没有缓冲区,缓冲字节流(BufferedInputStream)相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流(BufferedReader)相对于字符流效率提升就不是那么大了

功能区分-节点流&处理流

根据流功能区分,可以分为节点流和处理流。节点流是真正直接处理数据的(如FileInputStream);处理流是装饰加工节点流的。(**如BufferedReader**)

处理流是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如 BufferedReader。
处理流的构造方法总是要带一个其他的流对象做参数。

常见节点流:

  • 文件流:FileInputStream,FileOutputStrean,FileReader,FileWriter,它们都会直接操作文件,直接与 OS 底层交互。因此他们被称为节点流 ,注意:使用这几个流的对象之后,需要关闭流对象,因为 java 垃圾回收器不会主动回收。不过在 Java7 之后,可以在 try() 括号中打开流,最后程序会自动关闭流对象,不再需要显式地 close。

  • 数组流:ByteArrayInputStream,ByteArrayOutputStream,CharArrayReader,CharArrayWriter,对数组进行处理的节点流。

  • 字符串流:StringReader,StringWriter,其中 StringReader 能从 String 中读取数据并保存到 char 数组。

  • 管道流:PipedInputStream,PipedOutputStream,PipedReader,PipedWrite,对管道进行处理的节点流。

常见处理流:

  • 缓冲流 :BufferedInputStrean,BufferedOutputStream,BufferedReader ,BufferedWriter,需要父类作为参数构造,增加缓冲功能,避免频繁读写硬盘,可以初始化缓冲数据的大小,由于带了缓冲功能,所以就写数据的时候需要使用 flush 方法,另外,BufferedReader 提供一个 readLine( ) 方法可以读取一行,而 FileInputStream 和 FileReader 只能读取一个字节或者一个字符,因此 BufferedReader 也被称为行读取器。

  • 转换流:InputStreamReader,OutputStreamWriter,要 inputStream 或 OutputStream 作为参数,实现从字节流到字符流的转换,我们经常在读取键盘输入(System.in)或网络通信的时候,需要使用这两个类。

  • 数据流:DataInputStream,DataOutputStream,提供将基础数据类型写入到文件中,或者读取出来。

四个基本的抽象流

Java封装了基本的四个流抽象类,所有的流都继承这四个抽象类,后续源码讲解会讲到。

字节流 字符流
输入流 InputStream Reader
输出流 outputStream Writer
流中的数据 二进制字节(8位) Unicode字符(16位)

Java IO流体系

字节流/字符流 所属抽象
名称 描述
字节流 InputStream   ByteArrayInputStream 字节数组输入流,包含一个内部缓冲区(一个数组),该缓冲区包含从流中读取的字节数据。内部计数器跟踪read方法要提供的下一个字节
FileInputStream 文件字节输入流对文件数据以字节的形式进行读取操作,如读取图片视频等
PipedInputStream 管道输入流,让多线程可以通过管道进行线程间的通讯
FilterInputStream 过滤输入流,为基础流提供一些额外的功能,是一个处理流
DataInputStream 数据输入流,继承于FilterInputStream,允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型
BufferedInputStream 缓冲输入流,继承于FilterInputStream,作用是为另一个输入流添加一些功能。是一个处理流
ObjectInputStream 反序列化流,可以从流中读入一个用户自定义的对象
SequenceInputStream 合并流,用于从多个流中读取数据它是按顺序读取数据的,是一个处理流
OutputStream ByteArrayOutputStream 字节数组输出流,向 Byte 数组、和本地文件中写入数据
FileOutputStream 文件输出流,用于将数据写入文件
FilterOutputStream 过滤输出流,为基础流提供一些额外的功能,是一个处理流
DataOutputStream 数据输入流,继承于FilterIOutputStream,允许应用程序以与机器无关方式将原始Java数据类型写入输出流。
BufferedOutputStream 缓冲输出流,继承于FilterInputStream,作用是为另一个输出流添加一些功能。是一个处理流
PrintStream 打印输出流,提供了将数据写入另一个流的方法,是一个处理类
ObjectOutputStream 序列化输出流,用于将原始数据类型和Java对象写入OutputStream
PipedOutputStream 管道输出流,让多线程可以通过管道进行线程间的通讯
字符流 Reader BufferedReader 和其子类负责装饰其它 Reader 对象
CharArrayReader 从Char数组读取数据
StringReader 从字符串中读取数据
FilterReader 所有自定义具体装饰流的父类,其子类 PushbackReader 对 Reader 对象进行装饰,会增加一个行号
InputStreamReader 连接字节流和字符流的桥梁,它将字节流转变为字符流
FileReader 用于从文件读取数据。
PipedReader 从与其它线程共用的管道中读取数据。
Writer BufferedWriter 是一个装饰器为 Writer 提供缓冲功能
CharArrayWriter 向Char数组写数据
StringWriter 向String写数据
FileWriter 向文件写数据
FilterWriter 所有自定义具体装饰流的父类,其子类 PushbackWriter 对 Writer 对象进行装饰,会增加一个行号
OutputStreamWriter OutputStream 到 Writer 转换的桥梁
PipedWriter 向与其它线程共用的管道中写入数据。
PrintWriter 类似PrintOutputStream