一文理清 Java IO 流与IO网络模型:从基础到实战

在 Java 编程的世界里,IO 流如同数据传输的 “高速公路”,承担着程序与外部设备之间的数据交互重任。无论是读写文件、网络通信,还是处理其他外部资源,IO 流都是不可或缺的核心技术。同时,不同的 IO 网络模型也影响着数据交互的效率与性能。接下来,我们将深入剖析 Java IO 流的分类与应用,以及 BIO、NIO、AIO 网络模型的奥秘。

一、Java IO 流基础概念

1.1 什么是 IO 流

IO 流(Input/Output Stream)是 Java 中用于实现数据输入和输出操作的机制。输入流负责将外部数据读取到程序中,就像把外部世界的信息 “引入” 到程序的 “大脑”;输出流则负责将程序中的数据写入到外部设备,如同程序将处理好的信息 “发送” 到外部世界。例如,从文件中读取数据、向控制台输出信息、与网络服务器进行数据交换等,都离不开 IO 流的支持。

1.2 IO 流的分类维度

IO 流可以从多个维度进行分类:

  • 流向维度:分为输入流和输出流。输入流是数据进入程序的通道,输出流是数据离开程序的通道。
  • 操作单元维度:可分为字节流和字符流。字节流以字节(8 位二进制数据)为单位进行数据操作,适用于处理所有类型的数据,如图片、音频、视频等;字符流以字符(根据字符编码,一个字符可能由多个字节组成)为单位进行操作,专门用于处理文本数据,在处理中文等字符时更加方便高效。
  • 角色维度:分为节点流和处理流。节点流直接与数据源或目标相连,如FileInputStream和FileOutputStream直接操作文件;处理流则 “包裹” 在节点流之上,对数据进行加工处理,如BufferedInputStream和BufferedOutputStream可以提高数据读写性能。

二、Java IO 流的具体类型及使用

2.1 字节流

2.1.1 字节输入流 FileInputStream

FileInputStream用于从文件中读取字节数据。通过指定文件路径创建对象后,可使用read()方法读取数据。read()方法每次读取一个字节,返回值为读取到的字节数据(以 int 类型表示,范围为 0 - 255),当读取到文件末尾时返回 -1。

try (FileInputStream fis = new FileInputStream("test.txt")) {
    int data;
    while ((data = fis.read()) != -1) {
        System.out.print((char) data);
    }
} catch (Exception e) {
    e.printStackTrace();
}
 

2.1.2 字节输出流 FileOutputStream

FileOutputStream用于将字节数据写入文件。创建对象时,若文件不存在则创建新文件;若文件已存在,默认会覆盖原有内容。使用write()方法写入数据,可传入一个字节或字节数组。

try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    String message = "Hello, World!";
    byte[] data = message.getBytes();
    fos.write(data);
} catch (Exception e) {
    e.printStackTrace();
}
 

2.2 字符流

2.2.1 字符输入流 FileReader

FileReader是专门用于读取文本文件的字符输入流。它内部基于FileInputStream,自动处理了字节到字符的转换。使用read()方法读取字符,每次读取一个字符,返回值为读取到的字符(以 int 类型表示,实际为字符的 Unicode 编码值),文件末尾返回 -1。

try (FileReader reader = new FileReader("test.txt")) {
    int character;
    while ((character = reader.read()) != -1) {
        System.out.print((char) character);
    }
} catch (Exception e) {
    e.printStackTrace();
}
 

2.2.2 字符输出流 FileWriter

FileWriter用于将字符数据写入文件,同样自动处理字符到字节的转换。write()方法可写入单个字符、字符数组或字符串。

try (FileWriter writer = new FileWriter("output.txt")) {
    writer.write("这是一段写入文件的字符数据");
} catch (Exception e) {
    e.printStackTrace();
}
 

2.3 缓冲流

2.3.1 缓冲字节流
  • 字节缓冲输入流 BufferedInputStream:在FileInputStream基础上增加了缓冲机制,内部维护一个缓冲区。读取数据时,先将数据批量读取到缓冲区,后续读取操作直接从缓冲区获取,减少了对底层文件的频繁读取操作,提高了读取效率。
  • 字节缓冲输出流 BufferedOutputStream:写入数据时,先将数据写入缓冲区,当缓冲区满或调用flush()方法时,才将数据一次性写入目标文件,降低了磁盘 I/O 操作次数。
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"));
     BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) {
    
    int data;
    while ((data = bis.read()) != -1) {
        bos.write(data);
    }
    
} catch (Exception e) {
    e.printStackTrace();
}
 

2.3.2 缓冲字符流
  • 字符缓冲输入流 BufferedReader:除了具备缓冲功能外,还提供了readLine()方法,用于按行读取文本数据,非常方便处理文本文件。
  • 字符缓冲输出流 BufferedWriter:提供newline()方法,用于写入系统相关的换行符,以及write()方法用于写入字符数据。
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"));
     BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    
    String line;
    while ((line = br.readLine()) != null) {
        bw.write(line);
        bw.newLine();
    }
    
} catch (Exception e) {
    e.printStackTrace();
}
 

2.4 转换流

转换流用于在字节流和字符流之间进行转换。当我们希望使用字符流的方法处理字节流数据时,就需要用到转换流。

  • 字符转换输入流 InputStreamReader:将字节输入流转换为字符输入流,可指定字符编码,实现字节到字符的正确转换。
  • 字节转换输出流 OutputStreamWriter:将字符输出流转换为字节输出流,同样可指定字符编码。
try (InputStreamReader isr = new InputStreamReader(new FileInputStream("input.txt"), "UTF-8");
     OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")) {
    
    int data;
    while ((data = isr.read()) != -1) {
        osw.write(data);
    }
    
} catch (Exception e) {
    e.printStackTrace();
}
 

2.5 序列化流

序列化流用于将对象转换为字节序列(序列化),以便存储到文件或通过网络传输;反序列化流则用于将字节序列还原为对象。

  • 序列化流 ObjectOutputStream:使用writeObject()方法将对象写入输出流。要序列化的对象必须实现Serializable接口。
  • 反序列化流 ObjectInputStream:使用readObject()方法从输入流中读取并还原对象。
import java.io.*;

class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 省略getter和setter方法
}

public class SerializationExample {
    public static void main(String[] args) {
        // 序列化对象
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            Person person = new Person("Alice", 25);
            oos.writeObject(person);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 反序列化对象
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person deserializedPerson = (Person) ois.readObject();
            System.out.println("Deserialized Person: " + deserializedPerson.getName() + ", " + deserializedPerson.getAge());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
 

2.6 打印流

打印流用于方便地输出各种数据类型,提供了丰富的格式化输出方法。

  • 字节打印流 PrintStream:print()和println()方法可输出各种数据类型,printf()方法用于格式化输出,类似于 C 语言中的printf函数。PrintStream默认输出到控制台,也可指定输出目标,如文件。
  • 字符打印流 PrintWriter:功能与PrintStream类似,但用于处理字符数据。
try (PrintStream ps = new PrintStream("output.txt")) {
    ps.println("这是一个字节打印流输出的示例");
    ps.printf("格式化输出:%d, %s", 10, "字符串");
} catch (Exception e) {
    e.printStackTrace();
}

try (PrintWriter pw = new PrintWriter("output.txt")) {
    pw.println("这是一个字符打印流输出的示例");
    pw.printf("格式化输出:%d, %s", 20, "另一个字符串");
    pw.flush();
} catch (Exception e) {
    e.printStackTrace();
}
 

2.7 压缩流

压缩流用于实现文件的压缩和解压缩操作。

  • 压缩流 ZipInputStream:用于读取压缩文件(.zip 格式),通过getNextEntry()方法获取压缩包中的下一个文件或目录,isDirectory()方法判断当前条目是否为目录。
  • 解压缩流 ZipOutputStream:用于创建压缩文件,使用putNextEntry()方法开始写入一个新的文件或目录条目,write()方法写入数据。
import java.io.*;
import java.util.zip.*;

public class ZipExample {
    public static void main(String[] args) {
        // 压缩文件
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("example.zip"))) {
            File fileToCompress = new File("test.txt");
            zos.putNextEntry(new ZipEntry(fileToCompress.getName()));
            
            try (FileInputStream fis = new FileInputStream(fileToCompress)) {
                byte[] buffer = new byte[1024];
                int length;
                while ((length = fis.read(buffer)) > 0) {
                    zos.write(buffer, 0, length);
                }
            }
            zos.closeEntry();
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 解压文件
        try (ZipInputStream zis = new ZipInputStream(new FileInputStream("example.zip"))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                File outputFile = new File(entry.getName());
                if (entry.isDirectory()) {
                    outputFile.mkdirs();
                } else {
                    try (FileOutputStream fos = new FileOutputStream(outputFile)) {
                        byte[] buffer = new byte[1024];
                        int length;
                        while ((length = zis.read(buffer)) > 0) {
                            fos.write(buffer, 0, length);
                        }
                    }
                }
                zis.closeEntry();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
 

三、Java IO 网络模型

3.1 BIO 同步阻塞模型

BIO(Blocking I/O)是同步阻塞模型。在 BIO 中,一个线程只能处理一个 Socket 连接。当线程处理 Socket 时,必须等待对方发送数据或完成操作后才能继续执行后续逻辑。例如,有一家小餐馆,只有一个服务员(线程),每来一位顾客(Socket 连接),服务员就得全程服务这位顾客,从点菜到上菜,期间不能去招呼其他顾客。这种方式在高并发场景下会导致线程数量消耗多、内存资源消耗高、CPU 上下文切换频繁,只适用于连接请求较少的情况。

3.2 NIO 同步非阻塞模型

NIO(Non-Blocking I/O)是同步非阻塞模型。一个线程可以监听处理多个 Socket 连接。它将所有客户端SocketChannel通道注册到轮询器(如Selector)的 kernel 内核上,有一个线程去轮询所有channel,当某一个channel状态发生变化时(如可读或可写),就去执行连接或接收请求逻辑。当线程发起一个 I/O 操作时,如果数据尚未准备好,NIO 会立即返回。

为了减少内核空间与用户空间之间的频繁切换,NIO 使用了多路复用技术。多路复用就是内核一次查询所有用户空间传入的fd文件描述符,只进行两次内核与用户态之间的切换,这就好比一个烤串师傅(线程),不用一直守着一个烤串(Socket 连接),而是可以同时关注多个烤串(fd 文件描述符),等某个烤串差不多好了(fd 状态变化)再去处理。

轮询器有select、poll和epoll等。select对用户态传入的fd文件数量有限制,poll没有这个限制。而epoll性能最优,它在内核态实现了红黑树和双向链表,红黑树存可连接的fd文件,双向链表存fd状态修改的fd文件,双向链表的数据是由中断技术(类似于信号驱动)来自动存取的,即自动检测fd文件状态修改并放入双向链表中。

3.3 AIO 异步非阻塞模型

AIO(Asynchronous I/O)是异步非阻塞模型。内核监听到请求事件后,会自动去处理请求,不需要切换到用户态去执行。异步处理完后,会进行通知相应线程进行后续操作。就像点外卖,顾客(线程)下单后不用一直等着,商家(内核)做好外卖后会主动通知顾客来取餐(后续操作),大大提高了效率和用户体验。但是现在并没有普及AIO,主要使用的还是BIO。

3.4 三种网络模型的对比与选择

模型

编程复杂度

并发性能

适用场景

BIO

简单

连接请求少、对性能要求不高

NIO

较复杂

高并发、阻塞时间较短的场景

AIO

复杂

最高

高并发、对响应时间要求苛刻

在实际项目中,应根据具体的业务需求、并发量、性能要求等因素综合考虑,选择合适的 IO 网络模型。

四、博主总结

  • IO流(分类?IO网络模型?)
    • 字节流
      • 字节输入流:FileInputStream
      • 字节输出流:FileOutputStream
      • write()、read()
    • 字符流
      • 字符输入流FileReader
      • 字符输出流:FileWriter
      • write()、read()
    • 缓冲字节流
      • 字节缓冲输入流:BufferedInputStream
      • 字节缓冲输出流:BufferedOutputStream
      • write()、read()
    • 缓冲字符流
      • 字符缓冲输出流:BufferedReader
      • 字符缓冲输入流:BufferedWriter
      • write()、newline()、readline()
    • 转换流:字节流 想要使用 字符流中的方法
      • 字符转换输入流:InputStreamReader
      • 字节转换输出流:OutputStreamWriter
      • write()、read()
    • 序列化流
      • 序列化流:ObjectOutputStream
      • 反序列化流:ObejectInputStream
      • writeObject()、readObject()
    • 打印流
      • 字节打印流:PrintStream
      • 字符打印流:PrintWriter
      • write()、println()、print()、printf()、
    • 压缩流
      • 压缩流:ZipInputStream
      • 解压缩流:ZipOutputStream
      • getNextEntry()、isDirectory()、putNextEntry()、write()
    • IO网络模型 BIO,NIO,AIO区别?
      • 聊天池例子、烤串例子
      • BIO同步阻塞模型:一个线程只能处理一个Socket链接,线程在处理Socket时,必须等待对方发送数据或完成操作后才能继续执行后续逻辑。线程数量消耗多、内存资源消耗高、CPU上下文切换频繁。适用于线程链接请求少的情况
      • NIO同步非阻塞模型:
        • 一个线程监听处理多个Socket链接,将所有客户端SocketChannel通道注册到轮询器的kernel内核上,上面有一个线程去轮询所有channel,当某一个channel状态发生变化时就去执行连接或接收请求逻辑。
        • 执行过程中继续轮询,当线程发起一个I/O操作时,如果数据尚未准备好,NIO会立即返回。
        • 其中轮询涉及到内核空间与用户空间之间的切换问题,所以使用了多路复用,多路复用就是内核一次查询所有用户空间传入的fd文件描述符,只进行两次内核与用户态之间的切换,类比于拿烤串。
        • 轮询器分为select、poll它们区别就在于用户态传入的fd文件数量前者有限制、后者无限制。最好的是epoll,它在内核态实现了红黑树和双向链表,红黑树存可连接的fd文件,双向链表存fd状态修改的fd文件,双向链表的数据是由中断技术类似于信号驱动来自动存取的,中断技术在这里就是自动红黑树检测fd文件状态修改并放入双向链表中。
      • AIO异步非阻塞模型,内核监听到请求事件后自动去处理请求,不需要切换到用户态去执行,异步处理完后会进行通知相应线程进行后续操作。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值