当前位置: 首页 > news >正文

Java IO

“CSDN的朋友们好!曾续缘的技术档案正在更新——一个坚持写技术博客XX年的开发者,擅长把复杂问题简单化。点击头像关注,开启我们的技术缘分吧~”

Java I/O

Java I/O(输入/输出)是Java编程语言中至关重要的一部分,它提供了API用于数据的读取和写入,实现了与外部世界的交互。

通过Java I/O,程序可以从文件系统、网络资源和控制台等读取数据,也可以将数据写入到文件、数据库或其他目的地,实现了数据的持久化存储。

此外,Java I/O还允许程序访问外部资源,执行诸如网络通信、文件传输和数据处理等任务。

它还支持与操作系统和用户的交互,通过控制台输入/输出实现用户输入和程序结果的显示。

因此,Java I/O在实现数据读写、持久化存储、资源访问和系统交互等方面发挥着重要作用,是Java编程中不可或缺的一部分。

Java I/O的主要组成部分

Java I/O主要由流(包括字节流和字符流)、文件操作、网络通信和序列化等组成。

流提供了基本的数据读写功能,包括字节流和字符流,以及缓冲流用于提高性能。

文件操作包括File类和RandomAccessFile类,用于处理文件和目录。

网络通信通过Socket编程和Datagram编程实现,支持TCP和UDP协议。

序列化则通过ObjectInputStream和ObjectOutputStream实现对象的序列化和反序列化。

此外,NIO提供了更高效的I/O操作方式,包括缓冲区、通道和选择器。

还有格式化输入/输出、其他I/O类和接口等。

这些组成部分构成了Java I/O框架,为Java应用程序提供了全面的输入/输出解决方案。

Java I/O体系结构

抽象类

InputStreamOutputStreamReaderWriter是Java I/O体系结构中的四个核心抽象类,它们分别代表了字节流字符流的基础。

InputStream

InputStream是一个抽象类,它是所有字节输入流的超类。

它提供了一系列用于读取字节的方法,如:

  • int read():从输入流中读取下一个字节的数据。
  • int read(byte[] b):从输入流中读取最多b.length个字节的数据到一个字节数组中。
  • int read(byte[] b, int off, int len):从输入流中读取最多len个字节的数据到一个字节数组中,从数组的off位置开始存放。

InputStream还提供了skip(long n)available()等方法来跳过流中的数据或者检查可读字节数。

OutputStream

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

它提供了一系列用于写出字节的方法,如:

  • void write(int b):将指定的字节写入输出流。
  • void write(byte[] b):将字节数组中的所有字节写入输出流。
  • void write(byte[] b, int off, int len):将字节数组中从off开始的len个字节写入输出流。

OutputStream还提供了flush()方法来刷新输出流,确保所有缓冲的数据都被写出。

Reader

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

它提供了一系列用于读取字符的方法,如:

  • int read():从输入流中读取下一个字符的数据。
  • int read(char[] cbuf):从输入流中读取最多cbuf.length个字符的数据到一个字符数组中。
  • int read(char[] cbuf, int off, int len):从输入流中读取最多len个字符的数据到一个字符数组中,从数组的off位置开始存放。

Reader还提供了skip(long n)ready()等方法。

Writer

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

它提供了一系列用于写出字符的方法,如:

  • void write(int c):将指定的字符写入输出流。
  • void write(char[] cbuf):将字符数组中的所有字符写入输出流。
  • void write(char[] cbuf, int off, int len):将字符数组中从off开始的len个字符写入输出流。

Writer还提供了flush()append(CharSequence csq)等方法来刷新输出流或者添加字符序列。

这些抽象类为Java I/O操作提供了基础,并且有许多具体的子类来实现不同类型的输入和输出处理,例如FileInputStreamFileOutputStreamBufferedReaderPrintWriter等。

InputStreamOutputStreamReaderWriter是Java I/O流库中的四个核心抽象类,它们分别代表字节流和字符流两种不同类型的输入输出操作。以下是它们之间的主要区别:

字节流与字符流的区别

类型

  • 字节流 (InputStreamOutputStream) 以字节为单位进行读写操作。
  • 字符流 (ReaderWriter) 以字符为单位进行读写操作。

使用场景

  • 字节流适用于读写二进制数据,如图片、音频、视频文件等。
  • 字符流适用于读写文本数据,如字符串、XML文件、JSON文件等。

字符编码

  • 字节流不涉及字符编码转换,它们直接操作原始的字节。
  • 字符流在读写时会涉及到字符编码(如UTF-8、UTF-16等),因为它们需要将字符转换为字节或将字节转换为字符。

性能

  • 字节流通常比字符流更高效,因为它们不需要处理字符编码。
  • 字符流在处理文本数据时可能更高效,因为它们可以直接操作字符,减少了编码和解码的开销。

缓冲

  • 字节流通常使用BufferedInputStreamBufferedOutputStream进行缓冲。
  • 字符流通常使用BufferedReaderBufferedWriter进行缓冲。

具体实现类

文件I/O

FileInputStream/FileOutputStream

  • FileInputStream用于从文件中读取字节。
  • FileOutputStream用于向文件中写入字节。

FileReader/FileWriter

  • FileReader用于从文件中读取字符。
  • FileWriter用于向文件中写入字符。

网络I/O

SocketInputStream/SocketOutputStream

  • SocketInputStreamInputStream的子类,用于从套接字中读取数据。通常不直接使用,而是通过Socket类的方法来获取。
  • SocketOutputStreamOutputStream的子类,用于向套接字中写入数据。同样,通常不直接使用,而是通过Socket类的方法来获取。

SocketReader/SocketWriter

  • Java标准库中没有直接名为SocketReaderSocketWriter的类。通常,我们会使用InputStreamReaderOutputStreamWriter来包装从Socket获取的InputStreamOutputStream,以实现字符流I/O。

缓冲I/O

BufferedInputStream/BufferedOutputStream

  • BufferedInputStream用于从底层输入流中读取数据,并提供缓冲区以提高读取性能。
  • BufferedOutputStream用于向底层输出流中写入数据,并提供缓冲区以提高写入性能。

BufferedReader/BufferedWriter

  • BufferedReader用于从底层字符输入流中读取数据,并提供缓冲区以提高读取性能。
  • BufferedWriter用于向底层字符输出流中写入数据,并提供缓冲区以提高写入性能。

数据I/O

DataInputStream/DataOutputStream

  • DataInputStream用于读取基本Java数据类型(如int、float、long等)。
  • DataOutputStream用于写入基本Java数据类型。

对象I/O

ObjectInputStream/ObjectOutputStream

  • ObjectInputStream用于反序列化对象,即从流中读取Java对象。
  • ObjectOutputStream用于序列化对象,即将Java对象写入流中。

文件I/O操作

文件I/O操作是核心的I/O操作之一,涉及到文件的创建、读取、写入、删除和遍历等。

文件读写基本操作

字节流

字节流用于以字节为单位读写文件。FileInputStreamFileOutputStream是用于文件读写的字节流实现。

读取文件:

InputStream in = new FileInputStream("example.txt");
int data;
while ((data = in.read()) != -1) {
    // 处理读取到的字节
}
in.close();

写入文件:

OutputStream out = new FileOutputStream("example.txt");
byte[] bytes = "Hello, World!".getBytes();
out.write(bytes);
out.close();
字符流

字符流用于以字符为单位读写文件。FileReaderFileWriter是用于文件读写的字符流实现。

读取文件:

Reader reader = new FileReader("example.txt");
int charData;
while ((charData = reader.read()) != -1) {
    // 处理读取到的字符
}
reader.close();

写入文件:

Writer writer = new FileWriter("example.txt");
writer.write("Hello, World!");
writer.close();

文件操作

文件创建、删除、重命名

使用File类可以方便地创建、删除和重命名文件。

创建文件
File file = new File("example.txt");
if (!file.exists()) {
    try {
        file.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
删除文件
if (file.exists()) {
    file.delete();
}
重命名文件
File newFile = new File("newexample.txt");
if (file.exists()) {
    file.renameTo(newFile);
}
文件属性读取

File类提供了多种方法来获取文件的属性,如isFile()isDirectory()length()lastModified()等。

   System.out.println("Is file: " + file.isFile());
   System.out.println("Is directory: " + file.isDirectory());
   System.out.println("File size: " + file.length());
   System.out.println("Last modified: " + file.lastModified());
文件夹遍历

File类还提供了遍历文件夹的方法,如list()listFiles()

   File directory = new File(".");
   String[] files = directory.list();
   for (String fileName : files) {
       System.out.println(fileName);
   }

文件过滤器

FileFilter接口

FileFilter接口用于过滤文件列表。

   FileFilter filter = new FileFilter() {
       public boolean accept(File file) {
           return file.getName().endsWith(".txt");
       }
   };
   File[] txtFiles = directory.listFiles(filter);
FilenameFilter接口

FilenameFilter接口用于基于文件名过滤文件列表。

   FilenameFilter filter = new FilenameFilter() {
       public boolean accept(File dir, String name) {
           return name.endsWith(".txt");
       }
   };
   String[] txtFiles = directory.list(filter);

网络I/O操作

网络I/O操作是Java中用于在网络中发送和接收数据的操作。

Socket编程

TCP通信

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

Java通过SocketServerSocket类支持TCP通信。

客户端通过Socket类创建一个到服务器的连接,而服务器通过ServerSocket类等待客户端的连接。

例子:创建一个简单的TCP客户端和服务器。

  • 客户端:
    Socket socket = new Socket("localhost", 8080);
    OutputStream out = socket.getOutputStream();
    out.write("Hello, Server!".getBytes());
    out.close();
    socket.close();
    
  • 服务器:
    ServerSocket serverSocket = new ServerSocket(8080);
    Socket clientSocket = serverSocket.accept();
    InputStream in = clientSocket.getInputStream();
    byte[] buffer = new byte[1024];
    int bytesRead = in.read(buffer);
    String receivedData = new String(buffer, 0, bytesRead);
    System.out.println("Received: " + receivedData);
    in.close();
    clientSocket.close();
    serverSocket.close();
    

UDP通信

UDP(用户数据报协议)是一种无连接的、不可靠的、基于数据报的传输层通信协议。

Java通过DatagramSocketDatagramPacket类支持UDP通信。

例子:创建一个简单的UDP客户端和服务器。

  • 客户端:
    DatagramSocket socket = new DatagramSocket();
    String message = "Hello, Server!";
    InetAddress address = InetAddress.getByName("localhost");
    DatagramPacket packet = new DatagramPacket(message.getBytes(), message.length(), address, 8080);
    socket.send(packet);
    socket.close();
    
  • 服务器:
    DatagramSocket socket = new DatagramSocket(8080);
    byte[] buffer = new byte[1024];
    DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
    socket.receive(packet);
    String receivedData = new String(packet.getData(), 0, packet.getLength());
    System.out.println("Received: " + receivedData);
    socket.close();
    

URL处理

URL类

URL类用于表示统一资源定位符,可以解析URL并从中获取信息,如协议、主机名、端口、路径等。

例子:获取URL的各个部分。

URL url = new URL("http://www.example.com:8080/path/to/resource?query#fragment");
System.out.println("Protocol: " + url.getProtocol());
System.out.println("Host: " + url.getHost());
System.out.println("Port: " + url.getPort());
System.out.println("Path: " + url.getPath());

URLConnection类

URLConnection类用于建立与URL指定的资源的连接,并从中读取数据或向其写入数据。

例子:使用URLConnection读取网页内容。

URL url = new URL("http://www.example.com");
URLConnection connection = url.openConnection();
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}
reader.close();

InetAddress类

InetAddress类用于表示互联网协议(IP)地址。

可以使用InetAddress来获取本地主机地址、解析域名等。

例子:获取本地主机地址。

InetAddress address = InetAddress.getLocalHost();
System.out.println("Local Host Address: " + address.getHostAddress());

缓冲I/O操作

缓冲I/O操作在Java中用于提高I/O操作的效率。通过使用缓冲区,可以减少对物理设备的实际读写次数,从而提升性能。

缓冲流的作用

缓冲流通过内部维护一个缓冲区来减少对底层I/O设备的访问次数。当读取数据时,缓冲流会一次性从设备读取较多的数据到缓冲区中,然后用户可以从缓冲区中逐步读取;当写入数据时,缓冲流会先将数据写入到缓冲区中,当缓冲区满时再一次性将数据写入到设备中。

这种机制可以显著减少I/O操作次数,特别是在处理大量数据时,可以大幅提高I/O性能。

缓冲流通常应用于字节流和字符流。

BufferedInputStream/BufferedOutputStream

BufferedInputStream是一个装饰器类,它包装一个已有的InputStream,为它提供缓冲功能。当读取数据时,BufferedInputStream会尽可能地从底层流中读取更多的数据到缓冲区中,然后逐步提供这些数据给用户。

BufferedOutputStream也是一个装饰器类,它包装一个已有的OutputStream,为它提供缓冲功能。当写入数据时,BufferedOutputStream会先将数据写入到缓冲区中,当缓冲区满时或调用flush()方法时,再将缓冲区中的数据一次性写入到底层流中。

例子:使用BufferedInputStreamBufferedOutputStream读写文件。

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 (IOException e) {
    e.printStackTrace();
}

BufferedReader/BufferedWriter

BufferedReader是一个装饰器类,它包装一个已有的Reader,为它提供缓冲功能。BufferedReader提供了读取文本行的方法readLine(),这是它相对于BufferedInputStream的一个特色。

BufferedWriter也是一个装饰器类,它包装一个已有的Writer,为它提供缓冲功能。BufferedWriter提供了newLine()方法,用于写入一个行分隔符。

例子:使用BufferedReaderBufferedWriter读写文本文件。

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 (IOException e) {
    e.printStackTrace();
}

数据I/O操作

数据I/O操作在Java中用于读写基本数据类型(如int、float、double等)和字符串。

DataInputStream/DataOutputStream

DataInputStreamDataOutputStream是Java中用于读写基本数据类型和字符串的装饰器流。

DataInputStream用于从输入流中读取基本数据类型和字符串。

DataOutputStream用于将基本数据类型和字符串写入输出流。

这两个流通常用于网络编程中,因为它们提供了一种平台无关的方式来读写基本数据类型。

读写基本数据类型

使用DataInputStream,可以读取基本数据类型,如readInt(), readFloat(), readDouble()等。

使用DataOutputStream,可以写入基本数据类型,如writeInt(), writeFloat(), writeDouble()等。

例子:使用DataInputStreamDataOutputStream读写基本数据类型。

try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
    dos.writeInt(42);
    dos.writeFloat(3.14f);
    dos.writeDouble(2.71828);
} catch (IOException e) {
    e.printStackTrace();
}

try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
    int num = dis.readInt();
    float pi = dis.readFloat();
    double e = dis.readDouble();
    System.out.println("Num: " + num);
    System.out.println("Pi: " + pi);
    System.out.println("E: " + e);
} catch (IOException e) {
    e.printStackTrace();
}

读写字符串

使用DataInputStreamreadUTF()方法可以读取UTF-8编码的字符串。

使用DataOutputStreamwriteUTF(String str)方法可以写入UTF-8编码的字符串。

例子:使用DataInputStreamDataOutputStream读写字符串。

try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("strings.bin"))) {
    dos.writeUTF("Hello, World!");
    dos.writeUTF("你好,世界!");
} catch (IOException e) {
    e.printStackTrace();
}

try (DataInputStream dis = new DataInputStream(new FileInputStream("strings.bin"))) {
    String greeting = dis.readUTF();
    String greetingCN = dis.readUTF();
    System.out.println(greeting);
    System.out.println(greetingCN);
} catch (IOException e) {
    e.printStackTrace();
}

DataInputStreamDataOutputStream提供了一种方便的方式来处理基本数据类型和字符串,特别是当需要跨网络传输这些数据时。这些流确保了数据的平台无关性,使得不同平台上的Java应用程序可以无缝地交换数据。

对象I/O操作

对象I/O操作在Java中用于序列化和反序列化对象。

ObjectOutputStream/ObjectInputStream

ObjectOutputStream用于将对象序列化,即把对象的状态信息转换为字节流,以便可以将其保存到文件中或通过网络传输。

ObjectInputStream用于反序列化,即从字节流中恢复对象的状态信息。

这两个流是Java序列化机制的核心,允许对象在Java虚拟机之间传输和持久化。

序列化和反序列化

序列化是对象转换为字节流的过程,这样就可以将其保存到文件中或通过网络发送。

反序列化是字节流恢复为对象的过程,这样就可以在接收端重建对象。

要序列化的对象必须实现Serializable接口,这是一个标记接口,表示对象可以被序列化。

例子:使用ObjectOutputStream序列化对象,使用ObjectInputStream反序列化对象。

// 序列化对象
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser"))) {
    MyObject obj = new MyObject();
    oos.writeObject(obj);
} catch (IOException e) {
    e.printStackTrace();
}

// 反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.ser"))) {
    MyObject obj = (MyObject) ois.readObject();
    System.out.println(obj);
} catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
}

自定义序列化

有时,默认的序列化机制可能不适合某些对象,或者需要控制序列化的过程。

可以通过实现writeObject()readObject()方法来自定义序列化和反序列化过程。

这些方法应该被声明为private,并且必须处理所有需要序列化的字段。

在这些方法中,可以使用ObjectOutputStreamObjectInputStream提供的其他方法来序列化和反序列化字段。

例子:自定义序列化和反序列化过程。

private void writeObject(ObjectOutputStream out) throws IOException {
    // 自定义序列化逻辑
    out.defaultWriteObject(); // 写入非transient字段
    // 写入transient字段或其他需要自定义序列化的字段
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    // 自定义反序列化逻辑
    in.defaultReadObject(); // 读取非transient字段
    // 读取transient字段或其他需要自定义反序列化的字段
}

对象序列化和反序列化是Java中处理对象持久化和网络传输的重要机制。通过序列化,可以将对象的状态保存下来,并在需要时重新创建对象。这对于构建分布式系统、缓存对象状态以及实现对象的跨网络传输至关重要。

NIO(New I/O)

NIO(New I/O)是Java 1.4版本引入的一种新的I/O编程模型,它提供了一种更高效的方式来处理文件、网络和其他I/O操作。

NIO的引入主要是为了解决传统I/O操作中的性能瓶颈,特别是在处理大量并发连接时。

NIO简介

NIO提供了一种基于通道(Channel)和缓冲区(Buffer)的I/O操作模型。

通道是一种连接到IO源或目的地的抽象,它可以是非阻塞的,这意味着可以在一个通道上进行I/O操作而无需阻塞线程。

缓冲区是一个内存区域,用于数据的读取和写入。

NIO通过选择器(Selector)实现了多路复用,允许一个线程管理多个通道,从而提高了I/O操作的效率。

Buffer

Buffer是NIO中的一个核心概念,用于存储读取或写入的数据。

Buffer是一个固定大小的内存块,可以由不同的数据类型(如字节、字符等)填充。

Buffer提供了多种操作方法,如put()用于写入数据,get()用于读取数据,以及flip()clear()方法用于切换Buffer的状态。

缓冲区有多种类型,如ByteBufferCharBufferShortBuffer等,它们分别用于不同的数据类型。

Channel

Channel是NIO中的另一个核心概念,用于表示数据源或目的地。

Channel可以是文件通道(FileChannel)、网络通道(SocketChannel、ServerSocketChannel等)等。

通道可以是非阻塞的,这意味着可以同时处理多个通道,而不会阻塞线程。

通道提供了一系列操作方法,如read()write()用于读写数据,以及transferTo()transferFrom()用于将数据从一个通道传输到另一个通道。

Selector

Selector是NIO中的一个关键组件,用于实现多路复用。

选择器允许一个线程管理多个通道,并且可以同时处理多个通道上的事件。

选择器使用select()方法来监听通道上的事件,如读事件、写事件等。

当有事件发生时,选择器会返回一个包含事件通道的SelectionKey集合,然后可以对这些通道进行相应的操作。

NIO提供了一种更高效的方式来处理I/O操作,特别是在处理大量并发连接时。通过使用缓冲区、通道和选择器,可以实现更快的数据传输和更低的系统资源消耗。

NIO的引入标志着Java I/O模型的重大改进,并且被广泛应用于网络编程、文件操作和其他I/O密集型应用中。

NIO操作的步骤

使用Java NIO进行I/O操作的步骤大致如下

  1. 创建通道(Channel)
    • 根据需要选择合适的通道类型,如FileChannelSocketChannelServerSocketChannel等。
    • 可以通过不同的方式创建通道,例如通过文件路径、网络地址或现有的Socket对象。
  2. 创建缓冲区(Buffer)
    • 根据需要选择合适的缓冲区类型,如ByteBufferCharBufferShortBuffer等。
    • 可以通过调用allocate()方法创建缓冲区,也可以通过allocateDirect()方法创建直接缓冲区。
  3. 将缓冲区注册到选择器(Selector)
    • 创建一个Selector对象来管理通道。
    • 将通道注册到选择器上,并指定感兴趣的事件,如SelectionKey.OP_READSelectionKey.OP_WRITE等。
  4. 执行选择操作
    • 调用select()方法让选择器等待事件的发生。
    • 当事件发生时,选择器会返回一个包含SelectionKey的集合。
  5. 处理已选择的事件
    • 遍历SelectionKey集合,对于每个SelectionKey,检查其对应的事件类型。
    • 如果事件是可读的,则从通道中读取数据到缓冲区中。
    • 如果事件是可写的,则从缓冲区中写入数据到通道中。
  6. 关闭通道和缓冲区
    • 完成所有操作后,关闭通道和缓冲区以释放资源。

下面是一个简单的Java NIO文件复制示例代码:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class NioFileCopyExample {
    public static void main(String[] args) throws IOException {
        // 创建通道
        FileChannel inChannel = FileChannel.open(Paths.get("input.txt"), StandardOpenOption.READ_ONLY);
        FileChannel outChannel = FileChannel.open(Paths.get("output.txt"), StandardOpenOption.WRITE_ONLY, StandardOpenOption.CREATE_NEW);
        // 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(4096);
        // 将缓冲区注册到选择器
        inChannel.register(null, SelectionKey.OP_READ);
        outChannel.register(null, SelectionKey.OP_WRITE);
        // 执行选择操作
        while (inChannel.read(buffer) > 0) {
            buffer.flip();
            outChannel.write(buffer);
            buffer.clear();
        }
        // 关闭通道和缓冲区
        inChannel.close();
        outChannel.close();
    }
}

在这个例子中,我们使用FileChannel来进行文件复制,并使用Selector来管理通道。我们创建了一个ByteBuffer来存储读取的数据,并通过select()方法等待数据可读。当数据可读时,我们将其从输入通道读取到缓冲区,然后将其写入输出通道。最后,我们关闭了输入和输出通道。

多路复用

NIO(New I/O)的多路复用功能是其最核心的特点之一,它允许一个线程管理多个通道,同时能够响应这些通道上的事件,如读事件、写事件等。

选择器是NIO中实现多路复用的关键组件。它允许一个线程管理多个通道,并且可以同时处理多个通道上的事件。

选择器使用select()方法来监听通道上的事件。当有事件发生时,选择器会返回一个包含事件通道的SelectionKey集合。

步骤

NIO中实现多路复用的步骤如下:

  1. 注册通道(Channel)
    • 首先,需要创建一个或多个通道,如SocketChannelServerSocketChannel等。
    • 然后,将这些通道注册到选择器上,并指定感兴趣的事件类型,如SelectionKey.OP_READSelectionKey.OP_WRITE等。
  2. 执行选择操作
    • 调用select()方法让选择器等待事件的发生。
    • 当事件发生时,选择器会返回一个包含SelectionKey的集合。
  3. 处理已选择的事件
    • 遍历SelectionKey集合,对于每个SelectionKey,检查其对应的事件类型。
    • 如果事件是可读的,则从通道中读取数据。
    • 如果事件是可写的,则向通道中写入数据。
    • 如果事件是连接就绪的,则创建一个新的通道。
  4. 取消注册
    • 处理完事件后,从选择器中取消注册该通道,以避免重复处理相同的事件。

例子

下面是一个简单的Java NIO多路复用例子,用于演示如何使用Selector来管理多个SocketChannel,并响应这些通道上的事件。

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NioMultiplexingExample {
    public static void main(String[] args) throws Exception {
        // 创建选择器
        Selector selector = Selector.open();
        // 创建服务器通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
        // 注册服务器通道到选择器,并设置为可接收新连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 循环处理事件
        while (true) {
            // 调用select()方法让选择器等待事件的发生
            selector.select();
            // 获取选择器中已选中的SelectionKey集合
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            // 遍历SelectionKey集合
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove(); // 防止重复处理
                // 检查事件类型
                if (key.isAcceptable()) {
                    // 如果是可接受的连接事件
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ); // 注册新连接的通道
                } else if (key.isReadable()) {
                    // 如果是可读事件
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int readBytes = socketChannel.read(byteBuffer);
                    if (readBytes > 0) {
                        byteBuffer.flip();
                        System.out.println("Read: " + new String(byteBuffer.array(), 0, readBytes));
                        byteBuffer.clear();
                    }
                }
            }
        }
    }
}

这个例子中,我们创建了一个Selector来管理多个SocketChannel。我们将服务器通道绑定到一个端口,并设置为非阻塞模式。然后,我们注册服务器通道到选择器,并设置为可接收新连接。在循环中,我们调用select()方法让选择器等待事件的发生。当事件发生时,我们获取选择器中已选中的SelectionKey集合,并遍历这些SelectionKey

对于每个SelectionKey,我们检查事件类型。如果是可接受的连接事件,我们创建一个新的SocketChannel并注册到选择器。如果是可读事件,我们从通道中读取数据并打印出来。

零拷贝技术

零拷贝(Zero-Copy)是一种优化数据传输的技术,它减少了在数据传输过程中数据在内存中的复制次数。

传统的数据传输(如从一个文件复制到另一个文件,或者通过网络发送文件)涉及到多次数据复制和上下文切换:

  1. 数据从磁盘读取到操作系统内核的缓冲区。
  2. 数据从内核缓冲区复制到用户空间的缓冲区。
  3. 数据从用户空间的缓冲区复制回内核缓冲区,以便发送到网络或写入磁盘。

每次数据复制都需要CPU参与,并且涉及到用户空间和内核空间之间的上下文切换,这些都是耗时的操作。

零拷贝技术通过减少这些复制操作来优化这个过程。

实现方式

常见的零拷贝实现技术有:

  1. mmap(内存映射)
    • mmap通过将内核空间的一段内存地址映射到用户空间,使得应用程序可以直接访问这段内存,从而避免了数据在用户空间和内核空间之间的拷贝。
  2. sendfile
    • sendfile是Linux系统中常用的零拷贝技术,它允许操作系统直接将数据从文件系统传输到网络栈,而不需要通过用户空间。
  3. splice
    • splice函数用于在两个文件描述符之间移动数据,且这个过程是在内核中完成的,不需要用户空间的参与。
  4. DMA(直接内存访问)
    • DMA允许外设(如网卡)直接与内存进行数据传输,而不需要CPU的介入,从而减少了数据拷贝的次数。
  5. 写时复制(Copy-on-Write)
    • 在某些情况下,当多个进程需要读取相同的资源时,系统可以共享同一个物理页面,直到某个进程尝试修改数据时,才会创建一个新的页面进行拷贝。
  6. 聚集操作(Scatter/Gather I/O)
    • 聚集操作允许一次I/O操作读取多个不连续的内存区域或者写入到多个不连续的内存区域,这样也减少了数据拷贝的次数。

在Java中,零拷贝通常是通过NIO(New I/O)API实现的,特别是通过FileChannel类和SocketChannel类。这些类提供了一些方法,如transferTo()transferFrom(),它们允许数据直接在内核空间的缓冲区之间传输,而无需在用户空间和内核空间之间复制数据。

例如,使用transferTo()方法,数据可以直接从文件系统的缓存传输到网络栈的缓冲区,而不需要经过应用程序的内存空间。

零拷贝技术主要涉及到Java NIO(New I/O)包中的API。以下是一些实现Java零拷贝的方法:

  1. FileChannel.transferTo()FileChannel.transferFrom() 方法:

    这两个方法允许将数据直接从源通道传输到目标通道,而无需通过用户空间进行数据复制。例如,你可以使用 transferTo() 方法将数据从文件通道传输到网络套接字通道,从而实现文件的零拷贝传输。

    try (FileChannel fileChannel = FileChannel.open(Paths.get("sourceFile"), StandardOpenOption.READ);
         SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080))) {
        
        long position = 0;
        long count = fileChannel.size();
        
        fileChannel.transferTo(position, count, socketChannel);
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  2. MappedByteBuffer

    MappedByteBufferjava.nio 包中的一个类,它允许你将文件直接映射到内存中,从而可以像访问内存一样访问文件内容。这种方式可以减少读取文件时的系统调用和内存复制操作。

    try (RandomAccessFile file = new RandomAccessFile("sourceFile", "rw");
         FileChannel channel = file.getChannel()) {
        
        MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
        // 操作buffer,例如读取或写入数据
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  3. SocketChannelgatherscatter I/O:

    SocketChannel 支持分散读取(scatter read)和聚集写入(gather write)。分散读取允许从通道中读取的数据分散到多个缓冲区中,而聚集写入则允许将多个缓冲区的数据聚集到通道中。这样可以减少数据在用户空间和内核空间之间的复制。

    ByteBuffer header = ByteBuffer.allocate(128);
    ByteBuffer body = ByteBuffer.allocate(1024);
    
    ByteBuffer[] bufferArray = { header, body };
    
    try (SocketChannel socketChannel = SocketChannel.open()) {
        socketChannel.read(bufferArray);
        // 或者
        socketChannel.write(bufferArray);
    } catch (IOException e) {
        e.printStackTrace();
    }
    

I/O和NIO的区别

  1. 面向对象不同

    • IO是面向流的,这意味着数据是以流的形式处理的,一次处理的是字节。
    • NIO是面向缓冲区的,数据是以块的形式处理的,一次处理的是缓冲区。
  2. 模式不同

    • IO只有阻塞模式,这意味着当一个线程发起一个IO请求时,它会一直等待直到该请求完成。例如,当线程从socket读取数据时,线程会阻塞直到数据到达。
    • NIO支持阻塞和非阻塞模式。在非阻塞模式下,线程可以在发起IO请求后立即返回去做其他事情,而不是等待IO操作完成。
  3. 选择器不同

    • IO没有选择器的概念。
    • NIO引入了选择器(Selector),这允许一个单独的线程来监视多个输入通道,从而可以有效地管理大量的并发连接。
  4. 速度和性能

    • NIO的设计目标之一是为了让Java程序员可以实现高速I/O,而无需编写自定义的本机代码。NIO将最耗时的I/O操作(如填充和提取缓冲区)交由操作系统处理,因此可以显著提高速度。
  5. 通道(Channel)

    • IO中没有通道的概念,数据流通常是单向的。
    • NIO中引入了通道,通道是双向的,可以用来读取和写入数据。
  6. 数据处理方式

    • IO中,数据从流中读取时,一旦读取完毕,就不能再重新访问已经读取的数据,除非再次读取。
    • NIO中,数据是从缓冲区读取的,可以在缓冲区内前后移动,重读或修改数据。
  7. 可伸缩性和并发性

    • IO模型在高并发情况下可能效率低下,因为每个连接都需要一个独立的线程来处理。
    • NIO模型通过非阻塞IO和选择器的支持,能够在单个线程上处理多个连接,更适用于高并发场景。

工作线程数设置

在多核环境下合理设置BIO(阻塞I/O)和NIO(非阻塞I/O)的工作线程数是一个复杂的问题,需要根据具体的应用场景、系统资源、I/O 特性等因素来决定。

BIO线程数设置

CPU 密集型任务:

  • 线程数建议设置为 CPU 核心数的 1 倍到 1.5 倍。过多的线程会导致上下文切换开销增加,从而降低性能。

I/O 密集型任务:

  • 因为线程在等待 I/O 操作完成时会处于阻塞状态,所以可以设置更多的线程数。通常的经验公式是:线程数 = CPU 核心数 * (1 + 平均等待时间 / 平均工作时间)。这个公式假设等待时间远大于工作时间。

NIO线程数设置

NIO 通常用于网络编程,其线程模型与 BIO 不同,因为它使用较少的线程来管理多个并发连接。

事件循环(Event Loop)线程数:

  • 对于 NIO,通常使用一个或几个线程来处理所有的 I/O 事件(事件循环线程)。
  • 可以设置为 CPU 核心数的 2 倍左右,因为 NIO 线程通常不会像 BIO 那样长时间阻塞。

工作线程数:

  • 对于非网络处理的任务,可以设置一个线程池来处理业务逻辑。
  • 工作线程数可以根据任务是 CPU 密集型还是 I/O 密集型来调整,参考上述 BIO 的设置。

Java I/O性能优化

Java I/O性能优化是提高应用程序处理I/O操作效率的关键。以下是一些常用的Java I/O性能优化技术。

使用缓冲流

缓冲流使用内部缓冲区来减少对底层流的实际访问次数,从而提高I/O操作的效率。

使用BufferedInputStreamBufferedOutputStream可以减少对文件的直接访问,使用BufferedReaderBufferedWriter可以减少对文本文件的直接访问。

缓冲流特别适用于处理大量小数据块的情况,可以显著提高I/O性能。

使用NIO

NIO提供了基于通道(Channel)和缓冲区(Buffer)的I/O操作模型,它比传统的I/O操作更加高效。

NIO支持非阻塞I/O,这意味着可以同时处理多个通道,而不会阻塞线程。

NIO还提供了选择器(Selector)来实现多路复用,允许一个线程管理多个通道。

使用NIO可以提高网络编程、文件操作和其他I/O密集型应用的性能。

线程池

在进行I/O操作时,可以使用线程池来管理线程,而不是为每个I/O操作创建一个新的线程。

线程池可以重用已创建的线程,减少线程创建和销毁的开销,从而提高应用程序的性能。

使用线程池可以提高应用程序的响应性和吞吐量,特别是在处理大量并发请求时。

除了上述技术,还有一些其他的方法可以用于Java I/O性能优化。

  • 使用异步I/O:Java 7引入了异步I/O支持,允许应用程序异步地执行I/O操作,而不需要等待操作完成。
  • 减少上下文切换:上下文切换是线程切换时的一种开销,可以通过优化应用程序的设计来减少上下文切换的次数。
  • 避免频繁地创建和销毁资源:例如,使用try-with-resources语句来自动关闭资源,而不是显式地调用close()方法。
  • 优化文件访问模式:例如,使用顺序访问模式来减少磁盘寻道时间,或者使用随机访问模式来提高访问效率。

参考文章:https://cengxuyuan.cn

相关文章:

  • 【深度学习】分布偏移纠正
  • PHP场地预定小程序源码
  • 多表关联查询的优化
  • BERT 大模型
  • Grafana——Rocky9安装Grafana相关步骤记录
  • 【文献精读】AAAI24:FacetCRS:打破对话推荐系统中的“信息茧房”
  • 无第三方依赖 go 语言工具库
  • 1688商品评论API接口概述,json数据示例参考
  • Unity 手机屏幕适配
  • 简站主题:简洁、实用、SEO友好、安全性高和后期易于维护的wordpress主题
  • 记一次 Git Fetch 后切换分支为空的情况
  • oppo,汤臣倍健,康冠科技25届春招内推
  • 二、几何体BufferGeometry顶点笔记
  • 【MySQL高级】17 - MySQL中常用工具
  • 3.hadoop3.3.6 HA集群搭建
  • SpringBoot源码解析(十):应用上下文AnnotationConfigServletWebServerApplicationContext构造方法
  • 深度学习-1.简介
  • 深入探索 DeepSeek 在数据分析与可视化中的应用
  • qemu启动aarch64 linux+ buildroot + 应用程序
  • MAVSDK - Custom Mavlink处理
  • 北京韩美林艺术馆党支部书记郭莹病逝,终年40岁
  • 六省会共建交通枢纽集群,中部六省离经济“第五极”有多远?
  • 秦洪看盘|缩量回踩,积蓄叩关能量
  • 马上评|这种“维权”已经不算薅羊毛,涉嫌犯罪了
  • 国新办将就2025年4月份国民经济运行情况举行新闻发布会
  • 美国务卿鲁比奥将前往土耳其参加俄乌会谈