Yang's blog Yang's blog
首页
Java
密码学
机器学习
命令手册
关于
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

xiaoyang

编程爱好者
首页
Java
密码学
机器学习
命令手册
关于
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • SpringCloud

    • 微服务架构介绍
    • SpringCloud介绍
    • Spring Cloud:生产者与消费者
    • Spring Cloud Eureka:构建可靠的服务注册与发现
    • Spring Cloud Ribbon:负载均衡
    • Spring Cloud Fegin:服务调用
    • Spring Cloud Hystrix:熔断器
    • Spring Cloud Zuul:统一网关路由
    • Spring Cloud Config:配置中心
  • Java后端框架

    • LangChain4j

      • 介绍
      • 快速开始
      • Chat and Language Models
      • Chat Memory
      • Model Parameters
      • Response Streaming
      • AI Services
      • Agent
      • Tools (Function Calling)
      • RAG
      • Structured Outputs
      • Classification
      • Embedding (Vector) Stores
      • Image Models
      • Quarkus Integration
      • Spring Boot Integration
      • Kotlin Support
      • Logging
      • Observability
      • Testing and Evaluation
      • Model Context Protocol
  • 八股文

    • 操作系统
    • JVM介绍
    • Java多线程
    • Java集合框架
    • Java反射
    • JavaIO
      • 1. 概述
        • 1.1 I/O 的分类
      • 2. 文件概述
        • 2.1 File对象结构
        • 2.11 Serializable接口
        • 2.12 Comparable<File> 接口
        • 2.2 File 对象常用操作
        • 2.21 创建 `File` 对象
        • 2.22 文件/目录创建
        • 2.23 文件/目录的基本判断
        • 2.24 获取文件信息
        • 2.25 文件/目录删除
        • 2.26 遍历文件夹
      • 3. 从文件和流关系
        • 3.1 为什么需要流?
        • 3.2 文件如何转为流?
      • 4. 核心类和接口
        • 4.1 字节流
        • 4.2 字符流
        • 4.3 文件操作
        • 4.4 数据流和对象流
      • 5. 基本操作和使用场景
        • 5.1 文件读取
        • 5.2 文件写入
        • 5.3 对象序列化
      • 6. 缓冲流
        • 6.1 直接使用流的问题
        • 6.2 如何使用缓冲流?
        • (1)使用 `BufferedInputStream` 读取文件
        • (2)使用 `BufferedReader` 读取文本
        • (3)使用 `BufferedOutputStream` 写入文件
        • (4)使用 `BufferedWriter` 写入文本
      • 7. 设计模式和特性
        • 7.1 装饰者模式
        • 7.2 异常处理
        • 7.3 性能优化
      • 8. BIO、NIO 和 AIO
        • socket连接建立时,内核发生了什么?
        • 8.1 BIO(Blocking I/O)
        • 8.11 BIO 工作原理
        • 8.12 BIO 示例:
        • 8.2 NIO(Non-blocking I/O)
        • 8.21 NIO 工作原理
        • 8.22 NIO 示例
        • 8.3 AIO(Asynchronous I/O)
        • 8.31 AIO 工作原理
        • 8.32 AIO 示例
        • 8.4 BIO、NIO 和 AIO 的对比
      • 9. I/O多路复用机制
        • 9.1 select
        • 9.2 poll
        • 9.3 epoll (Linux特有)
    • Mybatis介绍
    • Spring介绍
    • SpringBoot介绍
    • Mysql
    • Redis
    • 数据结构
    • 云计算
    • 设计模式
    • 计算机网络
    • 锁核心类AQS
    • Nginx
  • 前端技术

    • 初识Vue3
    • Vue3数据双向绑定
    • Vue3生命周期
    • Vue-Router 组件
    • Pinia 集中式状态存储
  • 中间件

    • RocketMQ
  • 开发知识

    • 请求参数注解
    • 时间复杂度和空间复杂度
    • JSON序列化与反序列化
    • Timestamp vs Datetime
    • Java开发中必备能力单元测试
    • 正向代理和反向代理
    • 什么是VPN
    • 正则表达式
  • Java
  • 八股文
xiaoyang
2025-02-22
目录

JavaIO

# Java I/O 详解

# 1. 概述

Java 的 java.io 包提供了用于输入和输出的类和接口,用于处理文件操作、数据流、网络通信等。Java I/O 的设计基于**流(Stream)**的概念,通过流将数据从源(Source)传输到目标(Destination)。它支持多种数据源和目标,包括文件、内存缓冲区、网络套接字等。

# 1.1 I/O 的分类

Java I/O 主要分为两类:

  • 字节流(Byte Stream):以字节为单位操作数据,适用于处理二进制数据(如图片、音频、视频等)。主要基类是 InputStream 和 OutputStream。
  • 字符流(Character Stream):以字符为单位操作数据,适用于处理文本数据(如文本文件)。主要基类是 Reader 和 Writer。

此外,Java I/O 还支持:

  • 缓冲流:通过缓冲减少对底层系统的直接访问,提高效率。
  • 数据流:处理基本数据类型(如 int、double)和对象。
  • 文件操作:直接操作文件和目录。

# 2. 文件概述

文件(File)是存储数据的地方。Java中主要涉及对文件的输入和输出(I/O)操作,相对于 Java 内存,从磁盘读取文件是输入(Input),向磁盘写入文件是输出(Output)。Java 提供了 java.io.File 类来表示文件和目录,并提供了一系列操作方法,如创建文件、创建文件夹、判断文件是否存在、获取文件大小等。

# 2.1 File对象结构

在 Java 中,java.io.File 类实现了两个接口:

  1. Serializable 接口(java.io.Serializable)
  2. Comparable<File> 接口(java.lang.Comparable<File>)

# 2.11 Serializable接口

作用:

  • File 类实现了 Serializable,意味着 File 对象可以被序列化(即转换为字节流),并通过网络传输或存储到文件中。
  • 但需要注意的是,序列化 File 对象并不会序列化文件的内容,它只会存储 File 对象的路径信息。

示例:File 对象的序列化与反序列化

import java.io.*;

public class FileSerializableExample {
    public static void main(String[] args) {
        File file = new File("test.txt");

        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fileObject.ser"))) {
            oos.writeObject(file);
            System.out.println("File 对象已序列化");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fileObject.ser"))) {
            File deserializedFile = (File) ois.readObject();
            System.out.println("反序列化后的 File 对象路径: " + deserializedFile.getAbsolutePath());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

注意:

  • 反序列化后的 File 对象仍然指向原来的文件路径,但不会恢复文件内容。
  • 如果 test.txt 文件在磁盘上被删除,反序列化后的 File 仍然存在,但 file.exists() 会返回 false。

什么是序列化?

  • 序列化:把对象转换成数据(字节流,json等),方便存储或传输。但是Serializable序列化不推荐用于网络传输。具体因为:1. 只能 Java 内部使用,不支持跨语言。2. 体积大,性能低,数据包冗余。3. 存在安全漏洞,可能被攻击。
  • 反序列化:把数据转换回对象。

# 2.12 Comparable<File> 接口

作用:

  • 使 File 对象支持自然排序,可以通过 compareTo() 方法比较两个 File 对象的路径名(按字典顺序)。
  • 主要用于在 TreeSet<File> 或 Collections.sort(List<File>) 中对文件对象排序。

示例:File 对象排序

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class FileComparableExample {
    public static void main(String[] args) {
        List<File> fileList = new ArrayList<>();
        fileList.add(new File("C:/Users/A.txt"));
        fileList.add(new File("C:/Users/C.txt"));
        fileList.add(new File("C:/Users/B.txt"));

        // 按文件路径排序(字典序)
        Collections.sort(fileList);

        // 输出排序后的文件路径
        for (File file : fileList) {
            System.out.println(file.getPath());
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

compareTo() 方法规则:

  • 比较两个 File 对象时,会按照文件路径的字符串字典顺序进行排序。
  • 例如:"A.txt" < "B.txt" < "C.txt"。
  • 该方法区分大小写,"a.txt" 可能排在 "A.txt" 之后。

# 2.2 File 对象常用操作

java.io.File 是 Java 用于表示文件和目录的类,虽然它不能直接操作文件内容,但提供了文件管理的功能,例如创建、删除、判断文件是否存在等。其实这个过程涉及 磁盘和内存的交互


# 2.21 创建 File 对象

File 对象只是路径的抽象表示,不会真正创建文件或文件夹。

File file = new File("test.txt");  // 相对路径
File absoluteFile = new File("C:/Users/test.txt"); // 绝对路径
1
2

但这样不会真正创建文件! 需要调用 createNewFile() 或 mkdir() 进行创建。


# 2.22 文件/目录创建

(1)创建文件

import java.io.File;
import java.io.IOException;

public class CreateFileExample {
    public static void main(String[] args) {
        File file = new File("test.txt");

        try {
            if (file.createNewFile()) {
                System.out.println("文件创建成功: " + file.getAbsolutePath());
            } else {
                System.out.println("文件已存在");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

✅ createNewFile() 只有在文件不存在时才会创建,否则返回 false。

(2)创建目录(文件夹)

File dir = new File("myDir");

// 创建单个目录
if (dir.mkdir()) {
    System.out.println("目录创建成功");
}

// 创建多级目录
File multiLevelDir = new File("parentDir/childDir");
if (multiLevelDir.mkdirs()) {
    System.out.println("多级目录创建成功");
}
1
2
3
4
5
6
7
8
9
10
11
12

✅ mkdir() 只能创建单个目录,mkdirs() 可以创建多级目录。


# 2.23 文件/目录的基本判断

File file = new File("test.txt");

System.out.println("是否存在: " + file.exists());
System.out.println("是否是文件: " + file.isFile());
System.out.println("是否是目录: " + file.isDirectory());
System.out.println("是否可读: " + file.canRead());
System.out.println("是否可写: " + file.canWrite());
System.out.println("是否可执行: " + file.canExecute());
1
2
3
4
5
6
7
8

✅ 文件必须存在,isFile() 和 isDirectory() 才有意义。


# 2.24 获取文件信息

File file = new File("test.txt");

System.out.println("文件路径: " + file.getPath());
System.out.println("绝对路径: " + file.getAbsolutePath());
System.out.println("父级目录: " + file.getParent());
System.out.println("文件大小 (字节): " + file.length());
System.out.println("最后修改时间: " + new java.util.Date(file.lastModified()));
1
2
3
4
5
6
7

✅ length() 只能获取普通文件的大小,不能用于文件夹。


# 2.25 文件/目录删除

File file = new File("test.txt");

if (file.delete()) {
    System.out.println("文件删除成功");
} else {
    System.out.println("文件删除失败");
}
1
2
3
4
5
6
7

✅ 目录只有为空时才能删除,若要删除非空目录,需使用递归。

删除非空目录(递归实现)

public static void deleteDirectory(File dir) {
    if (dir.isDirectory()) {
        for (File file : dir.listFiles()) {
            deleteDirectory(file); // 递归删除子文件和子目录
        }
    }
    dir.delete();
}
1
2
3
4
5
6
7
8

# 2.26 遍历文件夹

File dir = new File("C:/Users");

if (dir.isDirectory()) {
    File[] files = dir.listFiles();
    for (File f : files) {
        System.out.println(f.getName());
    }
}
1
2
3
4
5
6
7
8

✅ listFiles() 返回 File[],可以获取目录下的所有文件和子目录。


# 3. 从文件和流关系

# 3.1 为什么需要流?

计算机中的文件是静态存储的数据,而程序运行时通常需要动态地读取或写入数据。直接操作文件不仅不方便,而且效率低下。因此,流(Stream) 作为数据的通道,让文件的数据可以像“水流”一样被程序处理。

可以把 文件 和 流 的关系类比成:

  • 文件: 一个水库,存储着大量数据(静态)。
  • 流: 一条水管,让程序能够获取或存入数据(动态)。

例如:

  • 输入流(InputStream/Reader):从文件读取数据到程序,就像从水库取水。
  • 输出流(OutputStream/Writer):将数据写入文件,就像向水库注水。

# 3.2 文件如何转为流?

在 Java 中,操作文件的流主要分为两类:

  • 字节流(InputStream/OutputStream):用于处理二进制文件,如图片、音频、视频等。
  • 字符流(Reader/Writer):用于处理文本文件,如 .txt 文件。

# 4. 核心类和接口

以下是 java.io 包中常用的核心类和接口:

# 4.1 字节流

  • InputStream:所有字节输入流的抽象基类,提供从数据源读取字节的方法。
    • 常用子类:
      • FileInputStream:从文件中读取字节。
      • ByteArrayInputStream:从字节数组中读取数据。
      • BufferedInputStream:缓冲输入流,减少对底层文件系统的直接调用。
  • OutputStream:所有字节输出流的抽象基类,提供向目标写入字节的方法。
    • 常用子类:
      • FileOutputStream:向文件写入字节。
      • ByteArrayOutputStream:将数据写入字节数组。
      • BufferedOutputStream:缓冲输出流。

# 4.2 字符流

  • Reader:所有字符输入流的抽象基类,提供读取字符的方法。
    • 常用子类:
      • FileReader:从文件中读取字符。
      • BufferedReader:缓冲字符输入流,提供 readLine() 方法读取整行文本。
      • InputStreamReader:将字节流转换为字符流(桥梁类)。
  • Writer:所有字符输出流的抽象基类,提供写入字符的方法。
    • 常用子类:
      • FileWriter:向文件写入字符。
      • BufferedWriter:缓冲字符输出流。
      • OutputStreamWriter:将字符流转换为字节流(桥梁类)。

# 4.3 文件操作

  • File:表示文件或目录的抽象路径,提供文件操作(如创建、删除、重命名)。
  • RandomAccessFile:支持随机读写文件,可以在文件中任意位置读写数据。

# 4.4 数据流和对象流

  • DataInputStream / DataOutputStream:读写基本数据类型(如 int、float)。
  • ObjectInputStream / ObjectOutputStream:读写对象(序列化和反序列化)。

# 5. 基本操作和使用场景

# 5.1 文件读取

  • 字节流示例(读取二进制文件):
import java.io.FileInputStream;
import java.io.IOException;

public class FileReadExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("example.bin")) {
            int byteData;
            while ((byteData = fis.read()) != -1) { // 读取单个字节
                System.out.print((char) byteData);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 字符流示例(读取文本文件):
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TextReadExample {
    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            while ((line = br.readLine()) != null) { // 逐行读取
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5.2 文件写入

  • 字节流示例(写入二进制文件):
import java.io.FileOutputStream;
import java.io.IOException;

public class FileWriteExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("output.bin")) {
            String data = "Hello, Java I/O!";
            fos.write(data.getBytes()); // 写入字节数组
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 字符流示例(写入文本文件):
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class TextWriteExample {
    public static void main(String[] args) {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
            bw.write("Hello, Java I/O!"); // 写入字符串
            bw.newLine(); // 新行
            bw.write("This is a test.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5.3 对象序列化

  • 序列化示例(保存对象到文件):
import java.io.*;

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

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

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 反序列化示例(从文件读取对象):
import java.io.*;

public class DeserializationExample {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.dat"))) {
            Person person = (Person) ois.readObject(); // 反序列化
            System.out.println("Name: " + person.name + ", Age: " + person.age);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

是的,在实际开发中,缓冲流(Buffered Stream) 被广泛使用,因为它可以 提高 I/O 读写性能,减少直接访问文件或网络的次数,提高程序的运行效率。


# 6. 缓冲流

# 6.1 直接使用流的问题

如果直接使用 FileInputStream 或 FileOutputStream 读取或写入文件,每次 read() 或 write() 操作都会触发磁盘 I/O,这会导致性能较低。例如:

FileInputStream fis = new FileInputStream("example.txt");
int data;
while ((data = fis.read()) != -1) {  // 每次读取一个字节
    System.out.print((char) data);
}
fis.close();
1
2
3
4
5
6

问题:

  • 每次 read() 只读取 一个字节,磁盘 I/O 频繁调用,性能低下。

解决方案:使用缓冲流

缓冲流使用 内存缓冲区 作为中转,每次 批量读取或写入数据,减少直接访问磁盘的次数,提升性能。

  • BufferedInputStream / BufferedOutputStream(缓冲字节流)
  • BufferedReader / BufferedWriter(缓冲字符流)

# 6.2 如何使用缓冲流?

# (1)使用 BufferedInputStream 读取文件

import java.io.*;

public class BufferedStreamExample {
    public static void main(String[] args) throws IOException {
        File file = new File("example.txt");

        // 用 BufferedInputStream 包装 FileInputStream
        FileInputStream fis = new FileInputStream(file);
        BufferedInputStream bis = new BufferedInputStream(fis);

        int data;
        while ((data = bis.read()) != -1) {  // 从缓冲区读取数据
            System.out.print((char) data);
        }

        bis.close();  // 关闭流
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

改进点:

  • BufferedInputStream 先从磁盘 批量读取数据 存入缓冲区,程序再从缓冲区 逐字节读取,减少磁盘 I/O 频率,提高性能。

# (2)使用 BufferedReader 读取文本

相比 BufferedInputStream 逐字节读取,BufferedReader 可以按行读取文本,更适合处理文本文件:

import java.io.*;

public class BufferedReaderExample {
    public static void main(String[] args) throws IOException {
        File file = new File("example.txt");

        // 用 BufferedReader 包装 FileReader
        FileReader fr = new FileReader(file);
        BufferedReader br = new BufferedReader(fr);

        String line;
        while ((line = br.readLine()) != null) {  // 按行读取
            System.out.println(line);
        }

        br.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

优点:

  • readLine() 直接读取一整行,避免每次 read() 读取一个字符,提高效率。

# (3)使用 BufferedOutputStream 写入文件

写入文件时,如果不使用缓冲流,每次 write() 调用都会触发磁盘 I/O,而缓冲流能 批量写入,减少磁盘操作:

import java.io.*;

public class BufferedOutputStreamExample {
    public static void main(String[] args) throws IOException {
        File file = new File("output.txt");

        // 用 BufferedOutputStream 包装 FileOutputStream
        FileOutputStream fos = new FileOutputStream(file);
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        String content = "Hello, BufferedOutputStream!";
        bos.write(content.getBytes());  // 批量写入数据
        bos.flush();  // 确保数据写入文件
        bos.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

改进点:

  • BufferedOutputStream 先写入缓冲区,等缓冲区满了才写入文件,减少磁盘 I/O。
  • flush() 强制写入缓冲区数据,避免数据丢失。

# (4)使用 BufferedWriter 写入文本

对于文本写入,可以使用 BufferedWriter,它支持 按行写入:

import java.io.*;

public class BufferedWriterExample {
    public static void main(String[] args) throws IOException {
        File file = new File("output.txt");

        // 用 BufferedWriter 包装 FileWriter
        FileWriter fw = new FileWriter(file);
        BufferedWriter bw = new BufferedWriter(fw);

        bw.write("Hello, BufferedWriter!");
        bw.newLine();  // 写入换行符
        bw.write("This is another line.");
        bw.flush();
        bw.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

优点:

  • newLine() 直接写入换行符,跨平台兼容。
  • flush() 确保缓冲区数据写入文件。

# 7. 设计模式和特性

# 7.1 装饰者模式

Java I/O 使用装饰者模式(Decorator Pattern),允许通过包装类增强流的功能。例如:

  • BufferedInputStream 装饰 FileInputStream,提供缓冲功能。
  • DataInputStream 装饰 BufferedInputStream,增加读写基本数据类型的能力。

示例:

FileInputStream fis = new FileInputStream("data.bin");
BufferedInputStream bis = new BufferedInputStream(fis); // 缓冲装饰
DataInputStream dis = new DataInputStream(bis); // 数据类型装饰
int value = dis.readInt(); // 读取整数
1
2
3
4

# 7.2 异常处理

I/O 操作通常涉及外部资源(如文件、网络),因此需要处理 IOException。推荐使用 try-with-resources 语句,确保资源正确关闭。

# 7.3 性能优化

  • 使用缓冲流(如 BufferedReader、BufferedOutputStream)减少底层系统调用。
  • 对于大数据量操作,合理设置缓冲区大小。

# 8. BIO、NIO 和 AIO

我们可以根据流的场景又可以分为

  1. 磁盘流(Disk Stream)和 内存流(Memory Stream)主要涉及本地数据的读写,如 文件操作(File I/O)。
  2. 网络流(Network Stream)则用于 远程数据传输,如 HTTP 通信、Socket 通信等。

在 Java 中,I/O 操作(如 网络通信、文件读写)对系统的 性能和并发能力 有着重要影响。三种主要的 I/O 模型:BIO(同步阻塞 I/O)、NIO(同步非阻塞 I/O)、AIO(异步非阻塞 I/O),它们在设计上各有不同,适用于不同的应用场景。

# socket连接建立时,内核发生了什么?

  1. **socket()**系统调用:

    • 创建一个socket,内核里会生成一个socket对象(struct socket)。
    • 这个对象里会关联一堆东西,比如:
      • 文件描述符(fd)
      • 协议族(TCP/UDP)
      • 发送缓冲区(send buffer)
      • 接收缓冲区(recv buffer)
      • 还有指向协议控制块(TCP层面的控制块,比如struct tcp_sock)
  2. **bind()**系统调用(服务器端):

    • 把这个socket绑定到某个本地IP和端口。
  3. **listen()**系统调用(服务器端):

    • 告诉内核,这个socket要变成监听状态了(监听连接)。
    • 内核为这个socket创建两个队列:
      • 半连接队列(syn queue):还没完成三次握手的连接。
      • 全连接队列(accept queue):三次握手完成,等待应用程序accept()拿走的连接。
  4. **connect()**系统调用(客户端):

    • 发起TCP三次握手。
    • 三次握手完成后,服务器端内核会把连接放入全连接队列。
  5. **accept()**系统调用(服务器端):

    • 应用调用accept()时,从全连接队列中取出一个连接。
    • 创建一个新的socket对象(代表新连接)。

# 8.1 BIO(Blocking I/O)

BIO(Blocking I/O)是 Java 中最传统的 I/O 模型。它的特点是:每个连接都会占用一个独立的线程,并且该线程会在执行 I/O 操作时被阻塞,直到数据准备好或操作完成。

# 8.11 BIO 工作原理

在 BIO 中,服务器需要 为每个客户端连接创建一个线程,每个线程都会进行阻塞式 I/O 操作,直到连接关闭或数据完成读取。这种模型的缺点在于,当并发量增加时,每个连接都需要一个线程,造成 线程资源浪费 和 上下文切换 开销。
调用I/O相关API(如accept()、read()、write())时,如果资源不可用,会阻塞,线程挂起,直到资源就绪。

  • accept() 阻塞:等新连接,是在等三次握手完成的新连接排队到accept队列。
  • read() 阻塞:等数据到达,用户态切换到内核态阻塞,监控fd文件状态变换。
  • write() 阻塞:等缓冲区可写,同理。

# 8.12 BIO 示例:

int listenfd = socket(...);
bind(listenfd, ...);
listen(listenfd, SOMAXCONN);

while (1) {
    int connfd = accept(listenfd, NULL, NULL);  // 阻塞,等新连接
    read(connfd, buffer, sizeof(buffer));       // 阻塞,等数据
    write(connfd, buffer, sizeof(buffer));      // 阻塞,等可写
}
1
2
3
4
5
6
7
8
9
  • 每个动作可能阻塞,线程被挂起。
  • 服务端吞吐量受限于线程数,性能差。
  • 常见优化:线程池模型(一个线程池去处理多个连接)。

# 8.2 NIO(Non-blocking I/O)

NIO(Non-blocking I/O)是相对于传统 BIO 的 非阻塞 I/O 模型,它通过 Selector(选择器) 和 Channel(通道) 的方式,使得一个线程可以同时处理多个客户端连接。NIO 模型使用事件驱动和非阻塞的方式来提高并发能力。

通过一个线程同时管理大量连接,不阻塞在单个I/O操作上。

  • 连接设置成非阻塞模式。
  • 依靠select/poll/epoll来统一监听所有fd。
  • 有事件时才处理,无事件时可以继续监听其他fd。

# 8.21 NIO 工作原理

在 NIO 中,一个线程可以监听多个通道(Channel),并使用 Selector 来处理多个连接。每个通道的 I/O 操作都是非阻塞的,线程只有在有事件发生时才会去处理 I/O 操作。NIO 提供了事件驱动的 I/O 操作,极大地减少了每个连接的线程开销。

# 8.22 NIO 示例

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false);

Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();  // 阻塞,监听所有注册fd的事件
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        if (key.isAcceptable()) {
            SocketChannel client = serverChannel.accept(); // 接受新连接
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            // 读数据
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 优点:
    • 一个线程可以处理多个连接,减少了线程的开销,适合高并发场景。
  • 缺点:
    • 相比 BIO,NIO 的编程模型复杂,需要理解 Selector 和 Channel 的使用。

BIO靠人海战术(线程堆积)解决并发,NIO靠机制(事件监听)解决并发;select/poll/epoll是I/O多路复用的具体实现方式,epoll效率最高。


# 8.3 AIO(Asynchronous I/O)

AIO(Asynchronous I/O)是 Java 7 引入的一种 异步 I/O 模型。与 NIO 的非阻塞模型不同,AIO 提供了一种真正的 异步处理方式,即 I/O 操作不会阻塞当前线程,而是通过回调通知机制在操作完成时通知应用程序。

# 8.31 AIO 工作原理

AIO 模型中的 I/O 操作在后台线程中执行,并且通过 回调函数(callback)通知应用程序操作完成。应用程序无需关注 I/O 操作的执行过程,而只需关注最终的结果。这使得 AIO 在处理大量 I/O 请求时比 NIO 更加高效。

# 8.32 AIO 示例

AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(8080));
serverSocket.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
    public void completed(AsynchronousSocketChannel result, Void attachment) {
        // 处理客户端请求
    }
    public void failed(Throwable exc, Void attachment) {
        exc.printStackTrace();
    }
});
1
2
3
4
5
6
7
8
9
10
  • 优点:
    • I/O 操作完全异步,CPU 和内存利用效率高,适合高并发、大量短连接的场景。
    • 不需要一直轮询 I/O 事件或阻塞线程,效率更高。
  • 缺点:
    • 编程模型复杂,理解和使用异步回调可能会增加开发难度。

# 8.4 BIO、NIO 和 AIO 的对比

特性 BIO NIO AIO
线程模型 一请求一个线程(阻塞) 多连接复用一个线程(非阻塞) 完全异步,通过回调处理(非阻塞)
操作方式 阻塞式(每个请求都阻塞等待) 非阻塞式(通过 Selector 轮询) 完全异步(后台线程处理,回调通知)
适用场景 小流量应用,低并发 高并发应用(如 Netty、Nginx) 高并发、大量短连接(如 WebSocket)
实现复杂度 简单 较复杂,需要理解 Selector 和 Channel 较复杂,需要理解异步回调机制
优点 简单实现,适用于低并发场景 高并发处理,线程复用,适合大规模并发 极高的效率,适用于高并发场景
缺点 高并发时性能差,资源消耗大 编程复杂,操作可能繁琐 编程复杂,异步回调可能难以理解

# 9. I/O多路复用机制


# 9.1 select

  • 传入一堆fd集合(最多1024个限制)。
  • 内核遍历每一个fd,看有没有准备好。
  • 每次调用都需要重新传递fd集合(开销大)。

特点:

  • 全量扫描fd,效率低。
  • fd数量大时,select性能急剧下降。

# 9.2 poll

  • 改进了select,没有1024限制。
  • 用链表存放fd,支持更多fd。
  • 同样是遍历检查所有fd,O(n)复杂度。
  • 同样需要每次重新传入fd数组。

# 9.3 epoll (Linux特有)

  • 真正高效的I/O多路复用。
  • 不需要每次传fd,内核维护fd集合。
  • fd变化通过epoll_ctl()进行添加/删除。
  • 内核内部用红黑树+就绪链表管理fd。
  • 事件触发机制可以是水平触发(LT)或边缘触发(ET)。
  • 复杂度是O(活跃fd数量),非常高效。
编辑 (opens new window)
上次更新: 2025/04/28, 14:54:37

← Java反射 Mybatis介绍→

最近更新
01
操作系统
03-18
02
Nginx
03-17
03
后端服务端主动推送消息的常见方式
03-11
更多文章>
Theme by Vdoing | Copyright © 2023-2025 xiaoyang | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式