BIO与NIO学习笔记
一、BIO(Blocking I/O)
1. BIO的概念
BIO(Blocking I/O)是传统的I/O操作模型,指的是在进行I/O操作时,线程会被阻塞,等待I/O操作完成。每个线程处理读写操作时对应一个socket,处理一个请求(io)。
2. BIO的特点
- 线程对应socket:每个线程都有一个socket,处理一个连接。
- 阻塞模型:线程在进行I/O操作时会被阻塞,等待数据读写完成。
- 上下文切换开销大:每次I/O操作需要切换到kernel态,开销较大。
- 内存占用高
3. BIO的工作流程
- 客户端发送读请求。
- 服务器线程等待数据到达。
- 服务器线程读取数据。
- 客户端线程处理数据。
4. 不阻塞处理的实现
如果想要实现不阻塞,可以通过创建子线程来处理I/O操作。例如:
public class BioServer {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept();
Runnable task = new Runnable() {
@Override
public void run() {
try {
byte[] data = new byte[1024];
int read = socket.read(data);
System.out.println("读取到数据:" + read);
} catch (IOException e) {
e.printStackTrace();
}
}
};
new Thread(task).start();
}
}
}
二、NIO(Non-Blocking I/O)
1. NIO的概念
NIO(Non-Blocking I/O)是现代I/O操作模型,允许一个线程同时处理多个I/O操作(因为有selector,所以不用创建多个线程)。通过selector和channel,实现多路复用,提升效率。
2. NIO的核心概念
- Selector(选择器):监控多个
channel的读写状态,非阻塞地通知事件。 - Channel(通道):替代传统的InputStream和OutputStream,支持非阻塞I/O操作。
- 多路复用(Multiplexing):通过Selector同时处理多个I/O事件。
下面这段代码是NIO创建ServerSocketChannel的代码:
- 创建
- 绑定端口号
- 设置非阻塞false,如果设置true就跟BIO差不多了
3. NIO的工作流程
- 创建Selector和多个通道。
- 注册通道到Selector。
- Selector等待I/O事件。
- 处理 Selector报告的事件(读、写、连接事件)。
4. NIO的优势
- 提高效率:一个线程可以同时处理多个I/O操作。
- 减少阻塞:避免线程上下文切换,性能更高。
- 灵活性:支持多种I/O操作方式。
- 减少内存:不需要多线程连接socket,可以一个selector来选择连接的客户端和服务端
5. 简单的NIO服务器示例(多路复用)
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 非阻塞
ssc.bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT); // 注册 Accept 事件
while (true) {
int readyChannels = selector.select(); // 阻塞,直到有事件
if (readyChannels == 0) continue;
// 获取所有就绪的事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) { // 接受新连接
ServerSocketChannel ssc1 = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc1.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ); // 注册 Read 事件
} else if (key.isReadable()) { // 读取数据
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = sc.read(buffer);
if(length == -1){
channel.close();
} else {
buffer.flip();
byte[] remainbuffer = new byte[buffer.remaining()];
buffer.get(remainbuffer);
System.out.println("收到数据: " + new String(buffer.array()));
}
}
}
keys.clear(); // 清空处理过的事件
}
流程:
selector.select()只监听注册过的 Channel。selectedKeys()遍历 就绪事件(Accept/Read/Write) 。- Accept(新连接) → 注册
OP_READ监听数据。 - Read(数据到达) → 处理数据。
其实在上面的代码中,我们能够看到NIO的三大组件channel、buffer、selector
- channel是一个一个的通道,大的通道叫ServerSocketChannel,小的叫SocketChannel;比如上面的代码中ssc是大的channel,readchannels就是小的channel
- buffer里面是读取数据的
- selector监听选择
上面列举的NIO方法是多路复用的方法,其实NIO还有轮询的办法。轮询的办法不需要注册channel
🔹 特点
- 不依赖
Selector,完全手动检查每个Channel的状态 - 非阻塞模式(
configureBlocking(false)) - 轮询所有连接(效率低,适合少量连接) 手动轮询的办法(不推荐使用)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
public class NioPollingServer {
public static void main(String[] args) throws IOException {
// 1. 开启非阻塞的 ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 非阻塞模式
ssc.bind(new InetSocketAddress(8080));
// 2. 维护所有客户端连接的列表
List<SocketChannel> clientChannels = new ArrayList<>();
System.out.println("服务端启动,监听 8080 端口...");
while (true) {
// 3. 手动轮询检查新连接
SocketChannel sc = ssc.accept(); // 非阻塞,如果没有连接会立即返回 null
if (sc != null) {
sc.configureBlocking(false); // 新客户端也要非阻塞
clientChannels.add(sc);
System.out.println("新客户端连接: " + sc.getRemoteAddress());
}
// 4. 手动轮询检查所有客户端是否有数据可读
for (SocketChannel client : clientChannels) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer); // 非阻塞读取
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("收到客户端数据: " + new String(data));
} else if (bytesRead == -1) {
// 客户端断开连接
System.out.println("客户端断开: " + client.getRemoteAddress());
client.close();
clientChannels.remove(client);
break; // 避免并发修改异常
}
// bytesRead == 0 表示当前无数据,继续轮询
}
// 5. 避免 CPU 空转(可选)
try {
Thread.sleep(100); // 减少轮询频率
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
三、BIO与NIO的对比
| 特点 | BIO | NIO |
|---|---|---|
| 模型 | 阻塞模型 | 非阻塞模型 |
| 线程模式 | 线程对应socket | 支持多线程同时处理多个I/O |
| 效率 | I/O操作阻塞,效率较低 | 非阻塞,效率更高 |
| 开发难度 | 简单实现,但线程开销大 | 复杂,需要学习Selector和Channel |
四、ThreadPool(线程池)
1. 线程池的概念
ThreadPool(线程池)是一种管理线程的工具类,允许开发者通过提交任务来执行多个线程,避免了手动管理线程。
2. 线程池的特点
- 线程复用:线程可以重复使用,减少线程创建的开销。
- 任务队列:支持任务的排队和执行,避免线程饥饿。
- 核心线程和线程池大小:可以配置核心线程数和最大线程数。
3. 简单的线程池实现代码
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class SimpleThreadPool {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
System.out.println(Thread.currentThread().getName() + "处理任务:" + i);
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "任务" + i);
}
executor.shutdown();
}
}
五、总结
- BIO:适合传统的单线程、单socket模型,简单实现但效率较低。
- NIO:适合需要高效处理多个I/O操作的场景,通过Selector和Channel实现多路复用,提升性能。
- 线程池:用于管理线程资源,避免线程饥饿和创建开销大问题,常用于BIO和NIO中。