Java IO与NIO的核心区别
Java 提供了三套 IO API:BIO(Blocking IO)、NIO(New IO)和 NIO.2(JDK 7+)。理解它们的差异对于高并发网络编程至关重要。
BIO:传统阻塞IO
ServerSocket server = new ServerSocket(8080); while (true) { Socket socket = server.accept(); new Thread(() -> { try { InputStream in = socket.getInputStream(); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) != -1) { } } catch (IOException e) { e.printStackTrace(); } }).start(); }
|
特点:
- 一个连接一个线程
- 简单易用,适合连接数少的场景
- 线程上下文切换开销大
NIO:非阻塞IO
NIO 三大核心组件:
1. Buffer(缓冲区)
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello".getBytes());
buffer.flip();
while (buffer.hasRemaining()) { byte b = buffer.get(); }
buffer.clear();
|
Buffer 状态变量:
position:当前读/写的位置
limit:读/写的上限
capacity:Buffer 的容量
2. Channel(通道)
Channel 是双向的,不同于 Stream 的单向:
FileChannel channel = new FileInputStream("file.txt").getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer);
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("localhost", 8080)); socketChannel.configureBlocking(false);
|
3. Selector(选择器)
Selector 允许单线程管理多个 Channel:
Selector selector = Selector.open(); ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(8080)); serverChannel.configureBlocking(false); serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) { selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); if (key.isAcceptable()) { SocketChannel client = serverChannel.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buf = ByteBuffer.allocate(1024); client.read(buf); } it.remove(); } }
|
NIO.2:异步IO(JDK 7+)
Path path = Paths.get("file.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path); ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("读取了 " + result + " 字节"); } @Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } });
|
三者对比
| 特性 |
BIO |
NIO |
NIO.2 (AIO) |
| IO模型 |
阻塞 |
非阻塞 |
异步 |
| 线程数 |
1连接1线程 |
1线程管理多连接 |
回调驱动 |
| 编程复杂度 |
简单 |
较复杂 |
中等 |
| 适用场景 |
连接少、并发低 |
连接多、并发高 |
超大量连接 |
| 典型应用 |
Tomcat传统模式 |
Netty |
少见 |
直接内存 vs 堆内存
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
|
直接内存特点:
- 读写速度快(避免 JVM 堆与 Native 堆之间的数据复制)
- 分配和回收成本高
- 适合长时间存活的大 Buffer
零拷贝(Zero Copy)
传统文件传输:
磁盘 → 内核缓冲区 → 用户缓冲区 → Socket缓冲区 → 网卡 (4次拷贝,4次上下文切换)
|
NIO 零拷贝(FileChannel.transferTo):
FileChannel source = new FileInputStream("source.txt").getChannel(); SocketChannel dest = SocketChannel.open();
source.transferTo(0, source.size(), dest);
|
实际应用
Netty 的线程模型
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new HttpServerCodec()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new MyHandler()); } });
|
Netty 基于 NIO 实现,采用 Reactor 模式:
- Boss EventLoop:处理 Accept 事件
- Worker EventLoop:处理 Read/Write 事件
总结
| 场景 |
推荐方案 |
| 简单文件读写 |
BIO(Files.readAllLines) |
| 高并发网络服务 |
NIO(Netty) |
| 需要零拷贝的文件传输 |
FileChannel.transferTo |
| 超大量并发连接 |
NIO + 多 Reactor |
理解 IO 模型是编写高性能 Java 应用的基础。对于现代高并发服务,基于 NIO 的 Netty 几乎是标准选择。