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体系结构
抽象类
InputStream
、OutputStream
、Reader
和Writer
是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操作提供了基础,并且有许多具体的子类来实现不同类型的输入和输出处理,例如FileInputStream
、FileOutputStream
、BufferedReader
和PrintWriter
等。
InputStream
、OutputStream
、Reader
和Writer
是Java I/O流库中的四个核心抽象类,它们分别代表字节流和字符流两种不同类型的输入输出操作。以下是它们之间的主要区别:
字节流与字符流的区别
类型:
- 字节流 (
InputStream
和OutputStream
) 以字节为单位进行读写操作。 - 字符流 (
Reader
和Writer
) 以字符为单位进行读写操作。
使用场景:
- 字节流适用于读写二进制数据,如图片、音频、视频文件等。
- 字符流适用于读写文本数据,如字符串、XML文件、JSON文件等。
字符编码:
- 字节流不涉及字符编码转换,它们直接操作原始的字节。
- 字符流在读写时会涉及到字符编码(如UTF-8、UTF-16等),因为它们需要将字符转换为字节或将字节转换为字符。
性能:
- 字节流通常比字符流更高效,因为它们不需要处理字符编码。
- 字符流在处理文本数据时可能更高效,因为它们可以直接操作字符,减少了编码和解码的开销。
缓冲:
- 字节流通常使用
BufferedInputStream
和BufferedOutputStream
进行缓冲。 - 字符流通常使用
BufferedReader
和BufferedWriter
进行缓冲。
具体实现类
文件I/O
FileInputStream/FileOutputStream:
FileInputStream
用于从文件中读取字节。FileOutputStream
用于向文件中写入字节。
FileReader/FileWriter:
FileReader
用于从文件中读取字符。FileWriter
用于向文件中写入字符。
网络I/O
SocketInputStream/SocketOutputStream:
SocketInputStream
是InputStream
的子类,用于从套接字中读取数据。通常不直接使用,而是通过Socket
类的方法来获取。SocketOutputStream
是OutputStream
的子类,用于向套接字中写入数据。同样,通常不直接使用,而是通过Socket
类的方法来获取。
SocketReader/SocketWriter:
- Java标准库中没有直接名为
SocketReader
和SocketWriter
的类。通常,我们会使用InputStreamReader
和OutputStreamWriter
来包装从Socket
获取的InputStream
和OutputStream
,以实现字符流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操作之一,涉及到文件的创建、读取、写入、删除和遍历等。
文件读写基本操作
字节流
字节流用于以字节为单位读写文件。FileInputStream
和FileOutputStream
是用于文件读写的字节流实现。
读取文件:
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();
字符流
字符流用于以字符为单位读写文件。FileReader
和FileWriter
是用于文件读写的字符流实现。
读取文件:
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通过Socket
和ServerSocket
类支持TCP通信。
客户端通过Socket
类创建一个到服务器的连接,而服务器通过ServerSocket
类等待客户端的连接。
例子:创建一个简单的TCP客户端和服务器。
- 客户端:java
Socket socket = new Socket("localhost", 8080); OutputStream out = socket.getOutputStream(); out.write("Hello, Server!".getBytes()); out.close(); socket.close();
- 服务器:
查看代码
javaServerSocket 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通过DatagramSocket
和DatagramPacket
类支持UDP通信。
例子:创建一个简单的UDP客户端和服务器。
- 客户端:java
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();
- 服务器:java
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()
方法时,再将缓冲区中的数据一次性写入到底层流中。
例子:使用BufferedInputStream
和BufferedOutputStream
读写文件。
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()
方法,用于写入一个行分隔符。
例子:使用BufferedReader
和BufferedWriter
读写文本文件。
查看代码
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
DataInputStream
和DataOutputStream
是Java中用于读写基本数据类型和字符串的装饰器流。
DataInputStream
用于从输入流中读取基本数据类型和字符串。
DataOutputStream
用于将基本数据类型和字符串写入输出流。
这两个流通常用于网络编程中,因为它们提供了一种平台无关的方式来读写基本数据类型。
读写基本数据类型
使用DataInputStream
,可以读取基本数据类型,如readInt()
, readFloat()
, readDouble()
等。
使用DataOutputStream
,可以写入基本数据类型,如writeInt()
, writeFloat()
, writeDouble()
等。
例子:使用DataInputStream
和DataOutputStream
读写基本数据类型。
查看代码
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();
}
读写字符串
使用DataInputStream
的readUTF()
方法可以读取UTF-8编码的字符串。
使用DataOutputStream
的writeUTF(String str)
方法可以写入UTF-8编码的字符串。
例子:使用DataInputStream
和DataOutputStream
读写字符串。
查看代码
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();
}
DataInputStream
和DataOutputStream
提供了一种方便的方式来处理基本数据类型和字符串,特别是当需要跨网络传输这些数据时。这些流确保了数据的平台无关性,使得不同平台上的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
,并且必须处理所有需要序列化的字段。
在这些方法中,可以使用ObjectOutputStream
和ObjectInputStream
提供的其他方法来序列化和反序列化字段。
例子:自定义序列化和反序列化过程。
查看代码
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的状态。
缓冲区有多种类型,如ByteBuffer
、CharBuffer
、ShortBuffer
等,它们分别用于不同的数据类型。
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操作的步骤大致如下
- 创建通道(Channel):
- 根据需要选择合适的通道类型,如
FileChannel
、SocketChannel
、ServerSocketChannel
等。 - 可以通过不同的方式创建通道,例如通过文件路径、网络地址或现有的
Socket
对象。
- 根据需要选择合适的通道类型,如
- 创建缓冲区(Buffer):
- 根据需要选择合适的缓冲区类型,如
ByteBuffer
、CharBuffer
、ShortBuffer
等。 - 可以通过调用
allocate()
方法创建缓冲区,也可以通过allocateDirect()
方法创建直接缓冲区。
- 根据需要选择合适的缓冲区类型,如
- 将缓冲区注册到选择器(Selector):
- 创建一个
Selector
对象来管理通道。 - 将通道注册到选择器上,并指定感兴趣的事件,如
SelectionKey.OP_READ
、SelectionKey.OP_WRITE
等。
- 创建一个
- 执行选择操作:
- 调用
select()
方法让选择器等待事件的发生。 - 当事件发生时,选择器会返回一个包含
SelectionKey
的集合。
- 调用
- 处理已选择的事件:
- 遍历
SelectionKey
集合,对于每个SelectionKey
,检查其对应的事件类型。 - 如果事件是可读的,则从通道中读取数据到缓冲区中。
- 如果事件是可写的,则从缓冲区中写入数据到通道中。
- 遍历
- 关闭通道和缓冲区:
- 完成所有操作后,关闭通道和缓冲区以释放资源。
下面是一个简单的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中实现多路复用的步骤如下:
- 注册通道(Channel):
- 首先,需要创建一个或多个通道,如
SocketChannel
、ServerSocketChannel
等。 - 然后,将这些通道注册到选择器上,并指定感兴趣的事件类型,如
SelectionKey.OP_READ
、SelectionKey.OP_WRITE
等。
- 首先,需要创建一个或多个通道,如
- 执行选择操作:
- 调用
select()
方法让选择器等待事件的发生。 - 当事件发生时,选择器会返回一个包含
SelectionKey
的集合。
- 调用
- 处理已选择的事件:
- 遍历
SelectionKey
集合,对于每个SelectionKey
,检查其对应的事件类型。 - 如果事件是可读的,则从通道中读取数据。
- 如果事件是可写的,则向通道中写入数据。
- 如果事件是连接就绪的,则创建一个新的通道。
- 遍历
- 取消注册:
- 处理完事件后,从选择器中取消注册该通道,以避免重复处理相同的事件。
例子
下面是一个简单的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)是一种优化数据传输的技术,它减少了在数据传输过程中数据在内存中的复制次数。
传统的数据传输(如从一个文件复制到另一个文件,或者通过网络发送文件)涉及到多次数据复制和上下文切换:
- 数据从磁盘读取到操作系统内核的缓冲区。
- 数据从内核缓冲区复制到用户空间的缓冲区。
- 数据从用户空间的缓冲区复制回内核缓冲区,以便发送到网络或写入磁盘。
每次数据复制都需要CPU参与,并且涉及到用户空间和内核空间之间的上下文切换,这些都是耗时的操作。
零拷贝技术通过减少这些复制操作来优化这个过程。
实现方式
常见的零拷贝实现技术有:
- mmap(内存映射):
- mmap通过将内核空间的一段内存地址映射到用户空间,使得应用程序可以直接访问这段内存,从而避免了数据在用户空间和内核空间之间的拷贝。
- sendfile:
- sendfile是Linux系统中常用的零拷贝技术,它允许操作系统直接将数据从文件系统传输到网络栈,而不需要通过用户空间。
- splice:
- splice函数用于在两个文件描述符之间移动数据,且这个过程是在内核中完成的,不需要用户空间的参与。
- DMA(直接内存访问):
- DMA允许外设(如网卡)直接与内存进行数据传输,而不需要CPU的介入,从而减少了数据拷贝的次数。
- 写时复制(Copy-on-Write):
- 在某些情况下,当多个进程需要读取相同的资源时,系统可以共享同一个物理页面,直到某个进程尝试修改数据时,才会创建一个新的页面进行拷贝。
- 聚集操作(Scatter/Gather I/O):
- 聚集操作允许一次I/O操作读取多个不连续的内存区域或者写入到多个不连续的内存区域,这样也减少了数据拷贝的次数。
在Java中,零拷贝通常是通过NIO(New I/O)API实现的,特别是通过FileChannel
类和SocketChannel
类。这些类提供了一些方法,如transferTo()
和transferFrom()
,它们允许数据直接在内核空间的缓冲区之间传输,而无需在用户空间和内核空间之间复制数据。
例如,使用transferTo()
方法,数据可以直接从文件系统的缓存传输到网络栈的缓冲区,而不需要经过应用程序的内存空间。
零拷贝技术主要涉及到Java NIO(New I/O)包中的API。以下是一些实现Java零拷贝的方法:
FileChannel.transferTo()
和FileChannel.transferFrom()
方法:这两个方法允许将数据直接从源通道传输到目标通道,而无需通过用户空间进行数据复制。例如,你可以使用
transferTo()
方法将数据从文件通道传输到网络套接字通道,从而实现文件的零拷贝传输。查看代码
javatry (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(); }
MappedByteBuffer
:MappedByteBuffer
是java.nio
包中的一个类,它允许你将文件直接映射到内存中,从而可以像访问内存一样访问文件内容。这种方式可以减少读取文件时的系统调用和内存复制操作。javatry (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(); }
SocketChannel
的gather
和scatter
I/O:SocketChannel
支持分散读取(scatter read)和聚集写入(gather write)。分散读取允许从通道中读取的数据分散到多个缓冲区中,而聚集写入则允许将多个缓冲区的数据聚集到通道中。这样可以减少数据在用户空间和内核空间之间的复制。查看代码
javaByteBuffer 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的区别
面向对象不同:
- IO是面向流的,这意味着数据是以流的形式处理的,一次处理的是字节。
- NIO是面向缓冲区的,数据是以块的形式处理的,一次处理的是缓冲区。
模式不同:
- IO只有阻塞模式,这意味着当一个线程发起一个IO请求时,它会一直等待直到该请求完成。例如,当线程从socket读取数据时,线程会阻塞直到数据到达。
- NIO支持阻塞和非阻塞模式。在非阻塞模式下,线程可以在发起IO请求后立即返回去做其他事情,而不是等待IO操作完成。
选择器不同:
- IO没有选择器的概念。
- NIO引入了选择器(Selector),这允许一个单独的线程来监视多个输入通道,从而可以有效地管理大量的并发连接。
速度和性能:
- NIO的设计目标之一是为了让Java程序员可以实现高速I/O,而无需编写自定义的本机代码。NIO将最耗时的I/O操作(如填充和提取缓冲区)交由操作系统处理,因此可以显著提高速度。
通道(Channel):
- IO中没有通道的概念,数据流通常是单向的。
- NIO中引入了通道,通道是双向的,可以用来读取和写入数据。
数据处理方式:
- IO中,数据从流中读取时,一旦读取完毕,就不能再重新访问已经读取的数据,除非再次读取。
- NIO中,数据是从缓冲区读取的,可以在缓冲区内前后移动,重读或修改数据。
可伸缩性和并发性:
- 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操作的效率。
使用BufferedInputStream
和BufferedOutputStream
可以减少对文件的直接访问,使用BufferedReader
和BufferedWriter
可以减少对文本文件的直接访问。
缓冲流特别适用于处理大量小数据块的情况,可以显著提高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()
方法。 - 优化文件访问模式:例如,使用顺序访问模式来减少磁盘寻道时间,或者使用随机访问模式来提高访问效率。