一、BIO 和 NIO
在我们之前的网络通信编程项目中,我们用的是传统的BIO 模型。但随着互联网高并发、大流量的需求升级,BIO 的瓶颈愈发明显, NIO 成为了高并发编程的核心方案。那么什么是 BIO?什么是 NIO?二者的核心特点与本质区别又是什么?
1.BIO
BIO 全称 Blocking IO,是我们最常使用的同步阻塞式 IO。用户线程主动发起 IO 请求,并且需要等待 IO 操作完成才能继续执行,IO 过程中线程会被挂起,无法做其他任务。结合我们之前的编程经验,它的一些特点可以总结如下:
- 阻塞贯穿全程:服务端 accept() 等待客户端连接时会阻塞;客户端连接后,read() / write() 读写数据时也会阻塞。
- 一连接一线程:每一个客户端连接,服务端都需要创建一个独立线程专门处理,连接断开线程销毁。
- 基于流(Stream)操作 :数据以字节流 / 字符流的形式传输,流是单向的。
- 无缓冲区:数据直接在程序和 IO 设备之间传输,没有中间缓冲层。
这些特点带来了 BIO 的一些原生缺陷:并发连接数过高时,会创建大量线程,导致服务器内存溢出、CPU 负载过高,同时线程闲置率却极高,资源利用率极低,导致它仅适合连接数少、并发量低的场景。
2.NIO
NIO 全称 New IO ,也被称为 Non-blocking IO(同步非阻塞 IO)。 它弥补了 BIO 的性能缺陷,是主流高并发网络框架的底层基础。它的一些核心特点如下:
- 非阻塞:线程发起连接、读写请求时,不会被挂起;如果没有数据 / 连接,会直接返回,线程可以去处理其他任务。
- 多路复用 :一个线程可以管理成百上千个客户端连接,通过**选择器(Selector)**监听所有连接的事件,有事件才处理。
- 基于缓冲区(Buffer) :NIO 最核心的设计,所有数据读写必须通过 Buffer,Buffer 是数据的载体,也是我们下一节重点讲解的内容。
- 基于通道(Channel) :数据通过 Channel 传输,Channel 是双向的(可读可写),效率远高于 BIO 的单向流。
对比 BIO,NIO 有一些明显机制优势:首先它线程数量极少,资源占用极低,而且非阻塞和多路复用使其并发能力呈指数级提升,因此它适合高并发、长连接、大量客户端的场景。
二、Buffer
1.四个核心参数
NIO 所有数据读写必须经过 Buffer ,Buffer 是 NIO 程序中最常用、最基础 的组件,所以我们本期先来讲解 Buffer 的四个核心参数。
1.capacity(容量) :缓冲区的固定最大大小 ,创建缓冲区时指定,一旦确定无法修改。代表缓冲区最多能存储多少个数据单元。
2.limit(界限 / 限制): 数据操作的上限边界。
- 写模式:表示最多可以写入多少数据
- 读模式:表示最多可以读取多少有效数据,limit 之后的位置,禁止读写。
3.position(当前位置): 读写指针的下一个操作位置,初始值为 0。 每写入 / 读取一个数据,position 自动向后移动一位,始终指向待操作的位置。
4.mark(标记): 备忘标记位,用于记录指定的 position 位置 。 调用 mark() 记录位置,调用 reset() 可以直接回到标记点,初始值为 -1(未设置标记)。
我们可以通过创建缓冲区和写入数据两步操作,直观观察参数的动态变化。
java
public class BufferDemo {
public static void main(String[] args) {
// 1. 分配容量为10的字节缓冲区,默认【写模式】
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("capacity:"+buffer.capacity());
System.out.println("limit:"+buffer.limit());
System.out.println("position:"+buffer.position());
// 2. 连续写入4个字节数据:A、B、C、D
buffer.put((byte)'A');
buffer.put((byte)'B');
buffer.put((byte)'C');
buffer.put((byte)'D');
System.out.println(" ------------------------ ");
System.out.println("capacity:"+buffer.capacity());
System.out.println("limit:"+buffer.limit());
System.out.println("position:"+buffer.position());
// 剩余可操作空间:limit - position
System.out.println("remaining:"+buffer.remaining());
System.out.println(" ------------------------ ");
}
}
首先执行 ByteBuffer.allocate 开辟一个固定容量为 10 的字节缓冲区,它默认处于写模式。 接着执行 4 次 put() 方法,写入字符 A,B,C,D:每调用 1 次 put() ,position 自动 +1 (指针向后移动一位),写入 4 个数据后,position 从 0 变为 4,而 capacity 和 limit 始终保持不变 。此外新增方法 remaining() 计算剩余可操作空间。
执行结果:

下面我们再来看一下 mark () 标记 和 reset () 重置 操作。
java
public class BufferDemo {
public static void main(String[] args) {
//
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("capacity:"+buffer.capacity());
System.out.println("limit:"+buffer.limit());
System.out.println("position:"+buffer.position());
buffer.put((byte)'A');
buffer.put((byte)'B');
buffer.mark();
buffer.put((byte)'C');
buffer.put((byte)'D');
System.out.println(" ------------------------ ");
// System.out.println("capacity:"+buffer.capacity());
// System.out.println("limit:"+buffer.limit());
// System.out.println("position:"+buffer.position());
// System.out.println("remaining:"+buffer.remaining());
System.out.println("limit:"+buffer.limit());
System.out.println("position1:"+buffer.position());
buffer.reset(); //回到标记位置
System.out.println("position2:"+buffer.position());
System.out.println("char : "+(char)buffer.get());
System.out.println("remaining:"+buffer.remaining());
System.out.println(" ------------------------ ");
}
}
我们在写入字符 A、B 后调用 mark(),将此时的 position 位置记录下来作为标记点,后续继续写入 C、D 使 position 向后移动,再通过 reset() 方法,就能让 position 直接回到之前 mark() 标记的位置,这两个方法就像给缓冲区添加了书签,可以随时回到指定的操作位置,配合 get() 读取数据,读取的是当前 position=2 位置的字符 C。

2.Buffer 读写切换
我们在上文掌握了写模式 的基础上,新增了flip () 读模式切换、hasRemaining() 遍历读取、rewind() 重置指针三个核心操作,这是 NIO Buffer从写入到读取的必备流程。
java
public class BufferDemo {
public static void main(String[] args) {
//
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("capacity:"+buffer.capacity());
System.out.println("limit:"+buffer.limit());
System.out.println("position:"+buffer.position());
buffer.put((byte)'A');
buffer.put((byte)'B');
buffer.put((byte)'C');
buffer.put((byte)'D');
System.out.println(" ------------------------ ");
System.out.println(" ------------------------ ");
buffer.flip(); //切换成读数据模型
//取出所有数据:计算缓冲区可读取数据个数
while(buffer.hasRemaining()){
System.out.println((char)buffer.get());
}
//重重 position
buffer.rewind();
System.out.println("rewind = "+(char)buffer.get());
}
}
flip() 是 Buffer 从写模式转为读模式的方法,调用后会将 limit 设置为写入结束后的 position 值(本例中为 4),同时把 position 重置为 0,以此划定有效数据的可读范围。之后通过 hasRemaining() 判断是否存在未读数据,配合 get() 方法循环遍历读取,每读取一次数据 position 就自动向后移动一位,依次打印出 A、B、C、D。最后调用 rewind() 方法重置读取指针,该方法会将 position 重新置为 0 且保持 limit 不变,让我们直接从头再次读取缓冲区内容,因此能够获取到字符 A。

以上是对于 Buffer 基础的讲解,之后我们还将学习选择器、通道这些其他 NIO 组件基础。