Java教程

Java学废之路07——IO流

本文主要是介绍Java学废之路07——IO流,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

七、IO流

对于任何程序设计语言而言,输入输出(Input/Output)系统都是非常核心的功能。程序运行需要数据,数据的获取往往需要跟外部系统进行通信,外部系统可能是文件、数据库、其他程序、网络、IO设备等等。外部系统比较复杂多变,那么我们有必要通过某种手段进行抽象、屏蔽外部的差异,从而实现更加便捷的编程。

I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道。在当今这个数据大爆炸时代,I/O 问题尤其突出,很容易成为一个性能瓶颈。正因如此,所以 Java 在 I/O 上也一直在做持续的优化。

IO分为两大类:

  • 文件IO
  • 网络IO

7.1 概述

7.1.1 IO常用类

在整个Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable。其中常用类如下:

说明
File文件类
RandomAccessFile随机存取文件类
InputStream字节输入流
OutputStream字节输出流
Reader字符输入流
Writer字符输出流
  1. File(文件特征与管理):用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。
  2. RandomAccessFile(随机文件操作):一个独立的类,直接继承至Object.它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作
  3. InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
  4. OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。
  5. Reader(文件格式操作):抽象类,基于字符的输入操作。
  6. Writer(文件格式操作):抽象类,基于字符的输出操作。

7.1.2 IO的体系结构

image-20200822102656259

Java I/O主要包括如下几个层次,包含三个部分:

1.流式部分――IO的主体部分;

2.非流式部分――主要包含一些辅助流式部分的类,如:File类、RandomAccessFile类和FileDescriptor等类;

3.其他类–文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。

7.2 File类

File类: 文件和文件目录路径的抽象表示形式,与平台无关

  • File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流
  • 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录
  • File对象可以作为参数传递给流的构造器

7.2.1 构造器

  • public File(String pathname):以pathname为路径创建File对象,可以是绝对路径或者相对路径:
    • 绝对路径: 是一个固定的路径,从盘符开始
    • 相对路径: 是相对于某个位置开始。IDEA中,main()方法中的相对路径指当前Project下;单元测试方法中的相对路径指当前Module下
  • public File(String parent,String child):以parent为父路径, child为子路径创建File对象。
  • public File(File parent,String child):根据一个父File对象和子文件路径创建File对象

7.2.2 常用方法

image-20201023164208991

image-20201023164317352

image-20201023164335851

image-20201023164354199

7.3 IO流

流的本质:数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

流的作用:为数据源和目的地建立一个输送通道,处理设备之间的数据传输。

在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。

7.3.1 IO流分类

image-20200822102927773

  • 根据处理数据单位的不同分为:字符流字节流

  • 根据数据流向不同分为:输入流输出流

  • 根据流的角色不同分为:节点流处理流

    • 节点流:直接从数据源或目的地读写数据

      image-20201023192050418

    • 处理流:不直接连接到数据源或目的地,而是“连接” 在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。如缓冲流等

      image-20201023192156341

(1)字节流与字符流

流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:

  • 字节流:数据流中最小的数据单元是字节
  • 字符流:数据流中最小的数据单元是字符(两个字节)。Java中字符是采用Unicode编码,一个字符是16位,即一个字符使用两个字节来表示,对多国语言支持性比较好。为此,JAVA中引入了处理字符的流。

字符流和字节流的使用范围:

  • 字节流一般用来处理图像,视频,以及PPT,Word类型的文件

  • 字符流一般用于处理纯文本类型的文件,如TXT文件等

字节流可以用来处理纯文本文件,但是字符流不能用于处理图像视频等非文本类型的文件。

(2)输入流与输出流

根据数据的输入、输出方向的不同对而将流分为输入流和输出流。

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

此输入、输出是相对于我们写的代码程序而言。

7.3.2 IO流对象

输入流输出流
字节流InputStreamOutputStream
字符流ReaderWriter
(1)输入字节流InputStream

image-20200822110007283

  • ByteArrayInputStream:字节数组输入流,该类的功能就是从字节数组(byte[])中进行以字节为单位的读取,也就是将资源文件都以字节的形式存入到该类中的字节数组中去
  • PipedInputStream:管道字节输入流,它和PipedOutputStream一起使用,能实现多线程间的管道通信
  • FilterInputStream :装饰者模式中处于装饰者,具体的装饰者都要继承它,所以在该类的子类下都是用来装饰别的流的,也就是处理类
    • BufferedInputStream:缓冲流,对处理流进行装饰,增强,内部会有一个缓存区,用来存放字节,每次都是将缓存区存满然后发送,而不是一个字节或两个字节这样发送,效率更高
    • DataInputStream:数据输入流,它是用来装饰其它输入流,它“允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”
  • FileInputSream:文件输入流。它通常用于对文件进行读取操作
    • File:对指定目录的文件进行操作。注意,该类虽然是在IO包下,但是并不继承自四大基础类
  • ObjectInputStream:对象输入流,用来提供对“基本数据或对象”的持久存储。通俗点讲,也就是能直接传输对象(反序列化中使用

//使用字节流FileInputStream处理文本文件,可能出现乱码。
@Test
public void testFileInputStream() {
    FileInputStream fis = null;
    try {
        //1. File实例化
        File file = new File("hello.txt");

        //2. 流实例化
        fis = new FileInputStream(file);

        //3.读数据
        byte[] buffer = new byte[5];
        int len;//记录每次读取的字节的个数
        while((len = fis.read(buffer)) != -1){

            String str = new String(buffer,0,len);
            System.out.print(str);

        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(fis != null){
            //4.关闭资源
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

}
(2)输出字节流OutputStream

image-20200822110646772

OutputStream 是所有的输出字节流的父类,它是一个抽象类

  • ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向Byte 数组、和本地文件中写入数据。PipedOutputStream 是向与其它线程共用的管道中写入数据
  • ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流(序列化中使用)。

注:在网络传输中使用的是字节流传输,所以需要序列化与反序列化


/*
实现对图片的复制操作
 */
@Test
public void testFileInputOutputStream()  {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        //
        File srcFile = new File("爱情与友情.jpg");
        File destFile = new File("爱情与友情2.jpg");

        //
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);

        //复制的过程
        byte[] buffer = new byte[5];
        int len;
        while((len = fis.read(buffer)) != -1){
            fos.write(buffer,0,len);
        }

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(fos != null){
            //
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(fis != null){
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

}
(3)输入字符流Reader

image-20200822111336095

Reader 是所有的输入字符流的父类,它是一个抽象类。

  • CharReader、StringReader 是两种基本的介质流,它们分别将Char 数组、String中读取数据。PipedReader 是从与其它线程共用的管道中读取数据。
  • BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它Reader 对象。
  • FilterReader 是所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,会增加一个行号。
  • InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream 转变为Reader 的方法。

/**
 * 使用字符输入流FileReader读取文件内容
 * 1. read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1
 * 2. 异常的处理:为了保证流资源一定可以执行关闭操作。需要使用try-catch-finally处理
 * 3. 读入的文件一定要存在,否则就会报FileNotFoundException。
 */
@Test
public void testFileReader() {
    //1 File类的实例化
    File file = new File("hello.txt"); // 参数为相对路径形式
    System.out.println(file.getAbsolutePath());

    //2 FileReader流的实例化
    FileReader reader = null;// 为了保证流资源的关闭,使用try-catch-finally把对流的操作包起来
    try {
        reader = new FileReader(file);      // 可能会报文件找不到异常

        //3 文件内容的读取
        // read()方法返回读取的一个字符。若读到文件末尾,则返回-1
        int data = reader.read();
        while (data != -1) {
            System.out.print((char) data);
            data = reader.read();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4 流的关闭
		//垃圾回收机制只回收JVM堆内存里的对象空间。
        //对其他物理连接,比如数据库连接、输入流输出流、Socket连接JVM无法进行自动回收
        try {
            if (reader != null)             // 可能会报空指针异常
                reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    } 
}
(4)输出字符流Writer

image-20200822111450176

Writer是所有的输出字符流的父类,它是一个抽象类。

  • CharArrayWriter、StringWriter是两种基本的介质流,它们分别向Char数组、String中写入数据。PipedWriter是向与其它线程共用的管道中写入数据
  • BufferedWriter是一个装饰器为Writer提供缓冲功能
  • PrintWriter和PrintStream极其类似,功能和使用也非常相似
  • OutputStreamWriter是OutputStream到Writer转换的桥梁,它的子类FileWriter其实就是一个实现此功能的具体类。功能和使用和OutputStream极其类似

/**
* 使用字符输出流FileWriter从内存中写出数据到文件
* 1. 输出操作,对应的File可以不存在的。并不会报异常
* 2. File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。
*    File对应的硬盘中的文件如果存在:
*      如果流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原有文件的覆盖
*      如果流使用的构造器是:FileWriter(file,true):不会对原有文件覆盖,而是在原有文件基础上追加内容
*/
@Test
public void testFileWriter() throws IOException {		
    // 使用throws解决异常,但是推荐使用try-catch-finally

    //1 File类实例化,指明要写出到的文件
    File file = new File("mine.txt");

    //2 字符输出流的实例化
    FileWriter fileWriter = new FileWriter(file);

    //3 写出操作
    fileWriter.write("1zdpzdpzdpzdpnb");
    fileWriter.write("2zdpzdpzdpzdpnb");
    
    //4 流的关闭
    fileWriter.close();

}

7.3.3 转换流

转换流的作用,文本文件在硬盘中以字节流的形式存储时,通过InputStreamReader读取后转化为字符流给程序处理,程序处理的字符流通过OutputStreamWriter转换为字节流保存。

(1)转换流的特点
  1. 其是字符流和字节流之间的桥梁
  2. 可对读取到的字节数据经过指定编码转换成字符
  3. 可对读取到的字符数据经过指定编码转换成字节
(2)何时使用转换流
  1. 当字节和字符之间有转换动作时;
  2. 流操作的数据需要编码或解码时。
(3)具体的对象体现
  • InputStreamReader:字节到字符的桥梁,将字节流以字符流输入。

  • OutputStreamWriter:字符到字节的桥梁,将字节流以字符流输出。

这两个流对象是字符体系中的成员,它们有转换作用,本身又是字符流,所以在构造时需要传入字节流对象进来。

(4)字节流与字符流的区别

字节流没有缓冲区,是直接输出的,而字符流是输出到缓冲区的。因此在输出时,字节流不调用close()方法时,信息已经输出了,而字符流只有在调用close()方法关闭缓冲区时,信息才输出。要想字符流在未关闭时输出信息,则需要手动调用flush()方法。

  • 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
  • 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

结论:只要是处理纯文本数据,就优先考虑使用字符流。除此之外都使用字节流

7.4 对象流Object

有的时候我们想要把一个Java对象变成字节流的形式传出去,有的时候我们想要从一个字节流中恢复一个Java对象。例如,有的时候我们想要把一个Java对象写入到硬盘或者传输到网路上面的其它计算机,这时我们就需要自己去通过java把相应的对象写成转换成字节流。对于这种通用的操作,可以使用序列化与反序列化来实现。

对象流ObjectInputStream与ObjectOutputStream用于处理存储与读取基本数据类型或对象的处理流。它的强大之处就是可以把对象写入到数据源中,也能把对象从数据源出还原回来。

  • 序列化:用ObjectOutputStream类保存基本类型数据或对象的机制【把Java对象转换为字节序列的过程】
  • 反序列化:用ObjectInputStream类读取基本类型数据或对象的机制【把字节序列恢复为Java对象的过程】

对象流ObjectInputStream与ObjectOutputStream不能序列化static和transient修饰的成员变量。

序列化可以在传递和保存对象时,保证对象的完整性和可传递性,对象转换为有序字节流可以使数据在网络中的传输或者保存至本地文件中变得更加方便。序列化与反序列化的核心是对象状态的保存与重建


7.4.1 序列化

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。

(1)序列化的应用

所有可能在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,比如RMI(Remote Method Invoke,即远程方法调用,是JavaEE的基础)过程中的参数和返回值;所有需要保存到磁盘里的对象的类都必须可序列化,比如Web应用中需要保存到 HttpSession 或 ServletContext 属性的Java对象。

因为序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是Java EE技术的基础——所有的分布式应用常常需要跨平台、跨网络,所以要求所有传递的参数、返回值必须实现序列化。**因此序列化机制是Java EE平台的基础。**通常建议:程序创建的每个JavaBean类都实现Serializable。

序列化的好处在于可将任何实现了Serialization接口的对象转化为字节数据,使其在保存和传输时可被还原。同时,序列化也是RMI(远程方法调用)的参数和返回值必须要实现的机制,因此序列化机制是RMI的基础。

(2)序列化的实现

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须要实现如下两个接口之一:

  • Serializable
  • Externalizable

如果某个类的属性不是基本数据类型【默认可被序列化】或 String 类型,而是另一个引用类型,那么这个引用类型也必须是可序列化的,否则拥有该类型的Field类也不能被序列化。

Serializable接口是一个标志性接口(Marker Interface),也就是说,该接口并不包含任何具体的方法,是一个空接口,仅仅用来判断该类是否能够序列化。

image-20210321202739534

其中被序列化的java对象是String类型的。因为String类本身实现了Serializable接口,所以可以被序列化:

image-20210321203811767

7.4.2 反序列化

读取网络传过来的或者是本地的字节流文件,还原为JAVA对象信息。

image-20210321203618167

7.4.3 自定义序列化

在一些特殊的场景下,如果一个类里包含的某些Field值是敏感信息,例如银行账户信息等,这时不希望系统将该Field值进行序列化;或者某个Field的类型是不可序列化的,因此不希望对该Field进行递归序列化,以避免引发
java.io.NotSerializableException异常。

此时,我们就需要自定义序列化了。自定义序列化的常用方式有两种:

  • 使用transient关键字
  • 重写writeObject与readObject方法
(1)transient关键字

transient关键字只能用于修饰Field,不可修饰Java程序中的其他成分。使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。

(2)重写writeObject与readObject方法

使用transient关键字修饰Field虽然简单、方便,但被transient修饰的Field将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java对象时无法取得该Field值。Java还提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各Field,甚至完全不序列化某些Field(与使用transient关键字的效果相同)。在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化。

private void writeObject(java.io.ObjectOutputStream out)
    throws IOException
private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;
private void readObjectNoData()
    throws ObjectStreamException;
  • writeObject()方法负责写入特定类的实例状态,以便相应的readObject()方法可以恢复它。通过重写该方法,程序员可以完全获得对序列化机制的控制,可以自主决定哪些Field需要序列化,需要怎样序列化。在默认情况下,该方法会调用out.defaultWriteObject来保存Java对象的各Field,从而可以实现序列化Java对象状态的目的。
  • readObject()方法负责从流中读取并恢复对象Field,通过重写该方法,程序员可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些Field,以及如何进行反序列化。在默认情况下,该方法会调用in.defaultReadObject来恢复Java对象的非静态和非瞬态Field。在通常情况下,readObject()方法与writeObject()方法对应,如果writeObject()方法中对Java对象的Field进行了一些处理,则应该在readObject()方法中对其Field进行相应的反处理,以便正确恢复该对象。
  • 当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。例如,接收方使用的反序列化类的版本不同于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。

7.4.4 serialVersionUID

凡是实现了Serializable接口的类都有一个表示序列化版本标识符的静态变量——serialVersionUID。

image-20210321204218101

serialVersionUID 用来表明累的不同版本之间的兼容性。其目的是以序列化对象进行版本控制,判断有关各版本反序列化时是否兼容。

image-20210321204521449

如果类没有显示地定义这个静态变量,该值将由JAVA运行时环境根据类的内部细节自动生成。若类的实例变量做了修改,serialVersionUID 可能会发生变化,故建议显式声明。

简单来说,序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本的一致性。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类中的serialVersionUID进行比较,如果相同就认为是一致的,就可以进行反序列化操作,否则就会出现序列化版本不一致的异常。

序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

(1)不设置serialVersionUID

image-20210321205733847

然后在依次执行序列化与反序列化操作,执行成功:

image-20210321205959276

修改自定义类Person,添加属性id后,直接去执行反序列化操作将会报错【因为在反序列化操作时,将找不到要还原的那个类,因此会还原失败】:

image-20210321210322384

(2)设置serialVersionUID

在Person中设置了 serialVersionUID 之后,要首先在原有不变的属性基础上进行一次序列化和反序列化操作,将UID与两个序列化操作进行绑定起来。然后再修改自定义类Person,添加属性id,然后直接去执行反序列化操作也将不会报错【因为有了版本一致性校验】:

image-20210321210848313

7.4.5 序列化的缺点

Java序列化存在四个致命缺点,导致其不适用于网络传输:

  • 无法跨语言:在网络传输中,经常会有异构语言的进程的交互,但Java序列化技术是Java语言内部的私有协议,其他语言无法进行反序列化。目前所有流行的RPC框架都没有使用Java序列化作为编解码框架。
  • 潜在风险高:不可信流的反序列化可能导致远程代码执行(RCE)、拒绝服务(DoS)和一系列其他攻击。
  • 序列化后的码流太大
  • 序列化的性能较低

在真正的生产环境中,一般会选择其它编解码框架,领先的跨平台结构化数据表示是 JSON 和 Protocol Buffers,也称为 protobuf。JSON 由 Douglas Crockford 设计用于浏览器与服务器通信,Protocol Buffers 由谷歌设计用于在其服务器之间存储和交换结构化数据。JSON 和 protobuf 之间最显著的区别是 JSON 是基于文本的,并且是人类可读的,而 protobuf 是二进制的,但效率更高。

7.5 IO模型(网络编程)

先来举个实例生活中的例子:

如果你想吃一份宫保鸡丁盖饭:

同步阻塞:你到饭馆点餐,然后在那等着,还要一边喊:好了没啊!

同步非阻塞:在饭馆点完餐,就去遛狗了。不过溜一会儿,就回饭馆喊一声:好了没啊!

异步阻塞:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自去拿。

异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心遛狗就可以了。

7.5.1 概要

  • 同步:指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪。自己上街买衣服,自己亲自干这件事,别的事干不了。
  • 异步:异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)。告诉朋友自己合适衣服的尺寸,大小,颜色,让朋友委托去卖,然后自己可以去干别的事。(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS)
  • 阻塞:所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止。去公交站充值,发现这个时候,充值员不在(可能上厕所去了),然后我们就在这里等待,一直等到充值员回来为止。
  • 非阻塞:非阻塞状态下, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待, 银行里取款办业务时,领取一张小票,领取完后我们自己可以玩玩手机,或者与别人聊聊天,当轮我们时,银行的喇叭会通知,这时候我们就可以去了。

7.5.2 分类

一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作。

同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO。

阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。

而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。

所以,IO操作可以分为3类:同步阻塞(即早期的BIO操作)、同步非阻塞(NIO)、异步非阻塞(AIO)。

  • 同步阻塞(BIO):
    在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。

  • 同步非阻塞(NIO):
    在此种方式下,用户进程发起一个IO操作以后便可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而导致不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。

  • 异步非阻塞(AIO):

    此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序。

7.5.3 Java的IO模型

  • 同步阻塞IO(JAVA BIO):
    同步并阻塞,服务器实现模式为一个连接对应一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

  • 同步非阻塞IO(Java NIO):

    同步非阻塞,服务器实现模式为一个请求对应一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。

  • 异步阻塞IO(Java NIO):
    此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性!

  • 异步非阻塞IO(Java AIO(NIO.2)):
    在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。

这篇关于Java学废之路07——IO流的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!