java IO(BIO)、NIO、AIO
什么是IO
什么是 I/O?
当程序需要从网络(如 TCP 套接字)读取数据,或向网络写入数据时,会触发 I/O 操作。
输入:从网络接收数据到应用程序的缓冲区(如 recv() 函数)。
输出:从应用程序的缓冲区发送数据到网络(如 send() 函数)。
为什么 I/O 是性能瓶颈?
等待时间:数据从网络传输到内存需要时间(如网络延迟、带宽限制)。
阻塞问题:传统的 I/O 操作会阻塞当前线程,导致资源无法被充分利用。
BIO
现实比喻
1.你去饭点点菜,点完之后只能在窗口等待,在做好之前,有其他人来了也不能接待,其他人只能干等待,这就是传统的同步阻塞代码。
流程图

BIO服务端代码例子
import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class BioServer { public static void main(String[] args) throws IOException { // 1. 创建 ServerSocket,绑定端口 8888 ServerSocket serverSocket = new ServerSocket(8888); System.out.println("BIO 服务端启动,监听端口 8888..."); // 2. 循环监听客户端连接 while (true) { // 阻塞点:等待客户端连接(线程挂起,直到有连接) Socket socket = serverSocket.accept(); System.out.println("客户端连接: " + socket.getRemoteSocketAddress()); // 3. 为每个客户端连接启动一个线程处理 new Thread(() -> handleClient(socket)).start(); } } private static void handleClient(Socket socket) { try ( // 4. 获取输入输出流 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(), true) ) { String message; // 5. 循环读取客户端发送的数据(阻塞点:read() 时线程挂起) while ((message = reader.readLine()) != null) { System.out.println("收到客户端消息: " + message); // 6. 向客户端返回响应 writer.println("服务端回复: " + message.toUpperCase()); } } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
BIO客户端代码例子
import java.io.*; import java.net.Socket; import java.util.Scanner; public class BioClient { public static void main(String[] args) throws IOException { // 1. 连接服务端 Socket socket = new Socket("localhost", 8888); System.out.println("连接到服务端..."); try ( // 2. 获取输入输出流 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(), true); Scanner scanner = new Scanner(System.in) ) { while (true) { // 3. 从控制台读取用户输入 System.out.print("请输入消息: "); String input = scanner.nextLine(); if ("exit".equalsIgnoreCase(input)) break; // 4. 向服务端发送消息(阻塞点:write() 时线程可能挂起) writer.println(input); // 5. 读取服务端响应(阻塞点:readLine() 时线程挂起) String response = reader.readLine(); System.out.println("服务端响应: " + response); } } finally { socket.close(); } } }
BIO 的核心问题
- 线程阻塞:
accept()、read()、write()等方法会阻塞当前线程,导致线程资源浪费。 - 高并发瓶颈:每个连接需要一个线程,线程数过多会导致内存和 CPU 资源耗尽。
- 适用场景:适合连接数较少且延迟不敏感的场景(如内部工具、小型应用)。
NIO
一个线程可以处理多个Socket连接
核心概念
SocketChannel 封装客户端的链接
Selector 轮询器 (select、poll、epoll)
Selector
1.由用户空间将fd传给内核空间由内核遍历获取可读状态的channel,然后将这些状态的结果从内核空间切换到用户空间再返回给轮训器,这一次调用可以获取到多个通道的状态。这些通道被一次调用获取到了就叫多路复用。
2.将用户空间遍历交给内核遍历
select 和poll的区别
轮训器将通道的fd描述符传给内核时数量是有限制的(也就是说不能一次把所有的fd传递给内核遍历),而poll没有数量限制,可以一次性都传递
epoll
1.创建ServerSocket
2.绑定端口号 bind
3.监听 listen
4.调用epplll_create(会在内核中开辟2个空间,红黑树、双向链表)
5.调用epoll_ctl(会将要监听的文件描述符放到红黑树空间,并标注监听的是可连接事件)
6.再调用epoll_wail遍历双向链表,获取可连接事件的fd
双向链表数据如何存储
设计中断知识如:
1.当网卡接收到数据时,内核就能检测到网络套接字的状态变化
2.网关设备驱动程序会通过内核会将数据放到对应的fd中的接收缓冲区并修改对应的红黑树文件描述符状态
3.内核就会检查红黑树中的fd文件描述符,发现对应的fd对应的状态是可连接的接着内核就会将该fd插入到双向链表,表示这个文件描述符准好被处理了。
4.这样调用epoll_wailt时就可以从双向链表中获取到了。
5.这样就不需要像select和poll在内核中遍历所有channel了,效率大大提高
代码例子
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; public class NioServer { public static void main(String[] args) throws IOException { // 1. 创建 Selector(事件监听器) Selector selector = Selector.open(); // 2. 创建 ServerSocketChannel(监听客户端连接的通道) ServerSocketChannel serverSocket = ServerSocketChannel.open(); serverSocket.bind(new InetSocketAddress("localhost", 8888)); serverSocket.configureBlocking(false); // 设置为非阻塞模式 // 3. 将 ServerSocketChannel 注册到 Selector,监听 OP_ACCEPT 事件(新连接) serverSocket.register(selector, SelectionKey.OP_ACCEPT); System.out.println("NIO 服务端启动,监听端口 8888..."); // 4. 事件循环(核心) while (true) { selector.select(); // 阻塞直到有事件就绪 Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); iter.remove(); // 处理完必须移除,避免重复处理 if (key.isAcceptable()) { // 处理新连接 handleAccept(key, selector); } else if (key.isReadable()) { // 处理读数据 handleRead(key); } } } } // 处理新连接 private static void handleAccept(SelectionKey key, Selector selector) throws IOException { ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel(); SocketChannel clientChannel = serverSocket.accept(); // 接受客户端连接 clientChannel.configureBlocking(false); // 设置为非阻塞模式 // 注册 OP_READ 事件,监听客户端发送的数据 clientChannel.register(selector, SelectionKey.OP_READ); System.out.println("客户端连接: " + clientChannel.getRemoteAddress()); } // 处理读数据 private static void handleRead(SelectionKey key) throws IOException { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配缓冲区 try { int bytesRead = clientChannel.read(buffer); // 读取数据到缓冲区 if (bytesRead == -1) { // 客户端关闭连接 clientChannel.close(); return; } buffer.flip(); // 切换为读模式 String message = new String(buffer.array(), 0, bytesRead).trim(); System.out.println("收到客户端消息: " + message); // 向客户端回写数据 String response = "服务端回复: " + message.toUpperCase(); ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes()); clientChannel.write(responseBuffer); } catch (IOException e) { clientChannel.close(); // 发生异常时关闭连接 } } }
AIO
linux并没有完全实现
与NIO最大区别
再NIO中通过selector获取到可写或者可读的程序后还是需要程序来处理,这种需要自己做的就是同步的意思
再AIO的情况下获取到可读或者可写的事件后内核主动来做,不需要程序来做。这就是异步

浙公网安备 33010602011771号