Java 中的 I/O(输入输出)操作可以分为 **阻塞 I/O** 和 **非阻塞 I/O**,它们主要区别在于:是否在数据准备好之前,线程会被阻塞。
### 1. **阻塞 I/O (Blocking I/O)**
在阻塞 I/O 中,线程发起 I/O 操作时,如果数据还没有准备好,线程会被阻塞,直到操作完成后才会继续执行。这意味着在等待 I/O 完成的过程中,线程处于“等待”状态,无法执行其他任务。
#### 案例:阻塞 I/O 示例
下面的示例使用 `InputStream` 读取文件,该操作是阻塞的,直到文件的内容被完全读取。
```java
import java.io.FileInputStream;
import java.io.IOException;
public class BlockingIOExample {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("example.txt")) {
int data;
// 逐字节读取文件数据,线程会阻塞直到数据被读取完
while ((data = inputStream.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
**解释**:
- `inputStream.read()` 是阻塞的。当程序调用 `read()` 方法时,如果没有数据可供读取,线程将被阻塞,直到有数据可供读取为止。
- 这种方法在小型 I/O 操作中是有效的,但对于大规模或高并发的应用程序来说,可能会降低效率,因为线程在等待 I/O 完成时会被挂起。
---
### 2. **非阻塞 I/O (Non-blocking I/O)**
非阻塞 I/O 是指线程发起 I/O 操作时,不需要等待数据准备好。如果数据没有准备好,I/O 操作立即返回,线程可以继续执行其他任务。非阻塞 I/O 常用于高并发场景,例如在网络编程中。
在 Java 中,非阻塞 I/O 通常使用 **NIO (New I/O)** 库来实现。NIO 提供了 **Channels** 和 **Selectors**,允许开发者以非阻塞的方式管理多个 I/O 操作。
#### 案例:非阻塞 I/O 示例
下面的示例使用 Java NIO 实现非阻塞 I/O,监听多个客户端的连接。
```java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
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 NonBlockingIOExample {
public static void main(String[] args) {
try {
// 打开一个 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 设置为非阻塞模式
// 创建 Selector
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 选择准备好的通道
selector.select();
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 接受新的客户端连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 读取客户端数据
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(256);
client.read(buffer);
String output = new String(buffer.array()).trim();
System.out.println("Message received: " + output);
}
// 移除已处理的键
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
**解释**:
- `ServerSocketChannel` 被设置为非阻塞模式(`configureBlocking(false)`),当没有连接可接受时,`accept()` 方法不会阻塞线程。
- 使用 `Selector` 对多个 `Channel` 进行管理。`Selector.select()` 会阻塞,直到至少有一个通道准备好进行 I/O 操作。
- `client.read(buffer)` 也是非阻塞的,如果没有数据可读,它会立即返回,而不是阻塞线程等待数据。
---
### **阻塞 I/O 与非阻塞 I/O 的对比**
| 特性 | 阻塞 I/O | 非阻塞 I/O |
|------------------------|--------------------------------------|--------------------------------------|
| **线程行为** | 当 I/O 操作未完成时,线程会被阻塞 | 线程不会阻塞,可以继续执行其他任务 |
| **实现** | 通过传统的 `InputStream`、`OutputStream` 等 | 使用 NIO 的 `Channel` 和 `Selector` |
| **使用场景** | 简单的 I/O 操作,低并发场景 | 高并发、大量 I/O 操作,如服务器开发 |
| **资源利用** | 资源利用效率低,线程容易被挂起 | 资源利用率高,线程可以处理多个 I/O 操作 |
---
### 总结
- **阻塞 I/O** 适合简单的 I/O 操作,例如读取文件或处理少量的网络请求。但在高并发场景中,阻塞 I/O 会因为线程等待而导致资源浪费。
- **非阻塞 I/O** 更加高效,适合处理大量并发请求的场景,例如高负载的网络服务器。NIO 提供的非阻塞 I/O 允许单个线程管理多个 I/O 通道,从而显著提高资源利用率。
希望这些解释和案例能帮助你理解 Java 中的阻塞与非阻塞 I/O。如果你还有其他问题或需要进一步解释,请随时告诉我。