NIO Buffer 与 Channel
学习目标
- 能解释 Buffer 的 capacity、position、limit 和 mark 状态。
- 会用 FileChannel 完成分块读写、传输和内存映射。
- 能判断 Selector、异步文件通道和普通流之间的适用边界。
核心知识点
专业术语
- Buffer:一段带状态游标的内存区域,Channel 通过它批量交换数据。
- Channel:可读、可写或可传输数据的 I/O 通道,例如
FileChannel、SocketChannel。 - position/limit/capacity:当前读写位置、有效边界和底层容量。
- Selector:把多个非阻塞网络 Channel 的就绪事件集中到一个线程处理。
白话解释与边界
Buffer 不是“自动增长的集合”,写入前要留意容量,读写模式转换要调用 flip。Channel 的一次 read 或 write 也可能只处理一部分数据,必须循环检查返回值。Selector 适合大量非阻塞网络连接,不会自动让普通文件 Channel 变成非阻塞;简单文件读写通常使用 Files 或阻塞式 FileChannel 更清晰。
常用用法
写入缓冲区:分配容量并推进游标
allocate 创建堆缓冲区,position 会随着写入前进;写入超出容量会抛 BufferOverflowException,容量要按协议或分块策略规划。
// 说明:写入缓冲区:分配容量并推进游标。
import java.nio.ByteBuffer;
public class ByteBufferPutDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.put((byte) 10).put((byte) 20);
// 关键变化:buffer.put((byte) 10).put((byte) 20);;依次写入 (byte) 10 和 (byte) 20,两个 put 都返回同一 ByteBuffer,position 前进到 2。
System.out.println(buffer.position() + "/" + buffer.limit() + "/" + buffer.capacity());
// 输出:2/4/4
}
}flip:从写模式切换到读模式
flip 把当前 position 变成 limit,再把 position 归零;每次写完准备读都要正确切换,否则读到的可能是空区间。
// 说明:flip:从写模式切换到读模式。
import java.nio.ByteBuffer;
public class ByteBufferFlipDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.put((byte) 10).put((byte) 20);
// 关键变化:buffer.put((byte) 10).put((byte) 20);;依次写入 (byte) 10 和 (byte) 20,两个 put 都返回同一 ByteBuffer,position 前进到 2。
buffer.flip();
// 关键变化:buffer.flip();;调用 flip,实参为 无显式参数;将 buffer 切换到读模式,position 置 0、limit 设为此前写入长度;缓冲状态更新。
System.out.println(buffer.get() + "," + buffer.get());
// 输出:10,20
}
}读取缓冲区:消费有效区间
hasRemaining 判断 position 是否小于 limit;相对 get() 会推进 position,绝对 get(index) 不会改变游标。
// 说明:读取缓冲区:消费有效区间。
import java.nio.ByteBuffer;
public class ByteBufferGetDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{1, 2, 3});
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
// 输出:循环依次打印 1、2、3。
}
}
}复用缓冲区:丢弃或保留未读数据
clear 丢弃尚未读取的内容并准备全量重写;compact 保留剩余内容并把它移到缓冲区开头,适合处理半包协议。
// 说明:复用缓冲区:丢弃或保留未读数据。
import java.nio.ByteBuffer;
public class ByteBufferReuseDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.put((byte) 1).put((byte) 2);
// 关键变化:buffer.put((byte) 1).put((byte) 2);;依次写入 (byte) 1 和 (byte) 2,两个 put 都返回同一 ByteBuffer,position 前进到 2。
buffer.flip();
// 关键变化:buffer.flip();;调用 flip,实参为 无显式参数;将 buffer 切换到读模式,position 置 0、limit 设为此前写入长度;缓冲状态更新。
System.out.println(buffer.get());
// 输出:1
buffer.compact();
// 关键变化:buffer.compact();;调用 compact,实参为 无显式参数;保留 buffer 的未读字节并切回写模式;position 移到剩余数据末端。
buffer.put((byte) 3).flip();
// 关键变化:buffer.put((byte) 3).flip();;compact 后 position 为 1,写入 3 后 position 为 2;flip 将 position 置 0、limit 置 2。
System.out.println(buffer.get() + "," + buffer.get());
// 输出:2,3
}
}回看缓冲区:重置或恢复读取位置
rewind 只把 position 归零并保留 limit,mark/reset 用于短暂回看;调用 clear、flip 等状态操作后 mark 可能失效。
// 说明:回看缓冲区:重置或恢复读取位置。
import java.nio.ByteBuffer;
public class ByteBufferMarkDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{1, 2, 3});
buffer.get();
// 关键变化:buffer.get();;读取 buffer 当前 position 的字节并推进 position;缓冲内容本身不变。
buffer.mark();
// 关键变化:buffer.mark();;调用 mark,实参为 无显式参数;记录 buffer 当前 position 作为 mark;后续 reset 可回到该位置。
int second = buffer.get();
// 关键变化:int second = buffer.get();;读取 buffer 当前 position 的字节 2 并写入 second;position 前进,缓冲内容不变。
buffer.reset();
// 关键变化:buffer.reset();;调用 reset,实参为 无显式参数;将 buffer 的 position 恢复到最近 mark。
System.out.println(second + "," + buffer.get());
// 输出:2,2
}
}FileChannel.read:分块读取文件
read 返回实际读到的字节数,-1 表示 EOF;不能假设一次 read 会填满 Buffer 或读完文件。
// 说明:FileChannel.read:分块读取文件。
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class FileChannelReadDemo {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("channel-read-", ".txt");
Files.writeString(file, "java");
// 关键变化:Files.writeString(file, "java");;将 "java" 写入路径 file;writeString 返回目标 Path,文件内容更新。
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
// 输入:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {;资源变量 channel 接收 FileChannel.open(file, StandardOpenOption.READ),try 结束时自动关闭。
// 关键变化:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {;创建资源 channel,构造表达式为 FileChannel.open(file, StandardOpenOption.READ);try 结束时关闭该资源。
ByteBuffer buffer = ByteBuffer.allocate(8);
int count = channel.read(buffer);
// 关键变化:int count = channel.read(buffer);;从文件读取 4 个字节“java”到 buffer,返回字节数 4 并写入 count;EOF 时才返回 -1。
System.out.println(count);
// 输出:4
}
Files.deleteIfExists(file);
// 关键变化:Files.deleteIfExists(file);;删除路径 file;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}FileChannel.write:循环写出 Buffer
write 也可能只消费部分 Buffer;循环 hasRemaining 是可靠写出模式。
// 说明:FileChannel.write:循环写出 Buffer。
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class FileChannelWriteDemo {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("channel-write-", ".txt");
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
// 输入:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {;资源变量 channel 接收 FileChannel.open(file, StandardOpenOption.WRITE),try 结束时自动关闭。
// 关键变化:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {;创建资源 channel,构造表达式为 FileChannel.open(file, StandardOpenOption.WRITE);try 结束时关闭该资源。
ByteBuffer buffer = ByteBuffer.wrap("java".getBytes());
while (buffer.hasRemaining()) {
channel.write(buffer);
// 关键变化:channel.write(buffer);;向 channel 写入 buffer 的剩余字节,返回本次写入字节数;buffer position 前进。
}
}
System.out.println(Files.size(file));
// 输出:4
Files.deleteIfExists(file);
// 关键变化:Files.deleteIfExists(file);;删除路径 file;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}需要强制落盘时再考虑 force 的成本。
FileChannel.position 与随机访问
随机访问适合固定格式文件和分块任务;多个线程共享同一 Channel 时要明确 position 是否共享,必要时使用带 position 参数的读写方法。
// 说明:FileChannel.position 与随机访问。
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class FileChannelPositionDemo {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("channel-position-", ".bin");
Files.write(file, new byte[]{10, 20, 30});
// 关键变化:Files.write(file, new byte[]{10, 20, 30});;将 new byte[]{10, 20, 30} 写入路径 file;write 返回目标 Path,文件内容更新。
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
// 输入:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {;资源变量 channel 接收 FileChannel.open(file, StandardOpenOption.READ),try 结束时自动关闭。
// 关键变化:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {;创建资源 channel,构造表达式为 FileChannel.open(file, StandardOpenOption.READ);try 结束时关闭该资源。
channel.position(1);
// 关键变化:channel.position(1);;调用 position,实参为 1;将 channel 的 position 设置为 1;位置状态更新。
ByteBuffer buffer = ByteBuffer.allocate(1);
channel.read(buffer);
// 关键变化:channel.read(buffer);;从 position=1 读取 1 个字节 20 到 buffer,返回 1;EOF 时才返回 -1。
buffer.flip();
// 关键变化:buffer.flip();;调用 flip,实参为 无显式参数;将 buffer 切换到读模式,position 置 0、limit 设为此前写入长度;缓冲状态更新。
System.out.println(buffer.get());
// 输出:20
}
Files.deleteIfExists(file);
// 关键变化:Files.deleteIfExists(file);;删除路径 file;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}通道间传输:减少用户态复制
通道传输可减少用户态复制,但返回值仍可能小于请求长度,跨平台和大文件场景要循环传输。
// 说明:通道间传输:减少用户态复制。
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class FileChannelTransferDemo {
public static void main(String[] args) throws Exception {
Path source = Files.createTempFile("channel-source-", ".bin");
Path target = Files.createTempFile("channel-target-", ".bin");
Files.write(source, new byte[]{1, 2, 3});
// 关键变化:Files.write(source, new byte[]{1, 2, 3});;将 new byte[]{1, 2, 3} 写入路径 source;write 返回目标 Path,文件内容更新。
try (var in = FileChannel.open(source, StandardOpenOption.READ);
// 输入:try (var in = FileChannel.open(source, StandardOpenOption.READ);;资源变量 in 接收 FileChannel.open(source, StandardOpenOption.READ,try 结束时自动关闭。
// 关键变化:try (var in = FileChannel.open(source, StandardOpenOption.READ);;创建资源 in,构造表达式为 FileChannel.open(source, StandardOpenOption.READ;try 结束时关闭该资源。
var out = FileChannel.open(target, StandardOpenOption.WRITE)) {
long copied = in.transferTo(0, in.size(), out);
// 关键变化:long copied = in.transferTo(0, in.size(), out);;把 source 文件从位置 0 开始的全部字节传给 out,返回复制字节数 3 并写入 copied。
System.out.println(copied);
// 输出:3
}
Files.deleteIfExists(source);
// 关键变化:Files.deleteIfExists(source);;删除路径 source;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
Files.deleteIfExists(target);
// 关键变化:Files.deleteIfExists(target);;删除路径 target;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}FileChannel.map:内存映射文件
映射适合随机访问大文件,但会占用虚拟地址空间,生命周期和刷盘语义也更复杂;不要把它当成所有文件读取的默认方案。
// 说明:FileChannel.map:内存映射文件。
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class MappedByteBufferDemo {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("channel-map-", ".bin");
Files.write(file, new byte[]{7});
// 关键变化:Files.write(file, new byte[]{7});;将 new byte[]{7} 写入路径 file;write 返回目标 Path,文件内容更新。
try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {
// 输入:try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {;资源变量 channel 接收 FileChannel.open(file, StandardOpenOption.READ),try 结束时自动关闭。
// 关键变化:try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {;创建资源 channel,构造表达式为 FileChannel.open(file, StandardOpenOption.READ);try 结束时关闭该资源。
var mapped = channel.map(FileChannel.MapMode.READ_ONLY, 0, 1);
// 关键变化:var mapped = channel.map(FileChannel.MapMode.READ_ONLY, 0, 1);;在流上调用 map 处理元素,结果写入 mapped。
System.out.println(mapped.get(0));
// 输出:7
}
Files.deleteIfExists(file);
// 关键变化:Files.deleteIfExists(file);;删除路径 file;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}Selector:注册非阻塞 Channel
Selector 只对支持非阻塞模式的网络 Channel 有意义;事件循环必须处理 key 失效、异常、读写部分完成和唤醒。
// 说明:Selector:注册非阻塞 Channel。
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
public class SelectorRegisterDemo {
public static void main(String[] args) throws Exception {
try (Selector selector = Selector.open(); SocketChannel channel = SocketChannel.open()) {
// 输入:try (Selector selector = Selector.open(); SocketChannel channel = SocketChannel.open()) {;资源变量 selector 接收 Selector.open(); SocketChannel channel = SocketChannel.open(),try 结束时自动关闭。
// 关键变化:try (Selector selector = Selector.open(); SocketChannel channel = SocketChannel.open()) {;创建资源 selector,构造表达式为 Selector.open(); SocketChannel channel = SocketChannel.open();try 结束时关闭该资源。
channel.configureBlocking(false);
// 关键变化:channel.configureBlocking(false);;把 channel 的阻塞模式设置为 false,通道状态更新。
channel.register(selector, SelectionKey.OP_CONNECT);
// 关键变化:channel.register(selector, SelectionKey.OP_CONNECT);;调用 register,实参为 selector, SelectionKey.OP_CONNECT;向 selector 注册 channel 的事件 SelectionKey.OP_CONNECT,返回 SelectionKey。
System.out.println(selector.keys().size());
// 输出:1
}
}
}不常用但需要知道
ByteBuffer.allocateDirect:堆外缓冲
Direct Buffer 可能减少 native I/O 的复制,但分配和回收成本更高;只有在长期、批量的底层 I/O 场景中经验证后才使用。
import java.nio.ByteBuffer;
public class DirectBufferDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocateDirect(4);
buffer.put((byte) 1).flip();
// 关键变化:buffer.put((byte) 1).flip();;put 写入 (byte) 1 后返回同一 ByteBuffer,flip 将 position 置 0、limit 置 1。
System.out.println(buffer.get());
// 输出:1
}
}创建缓冲区视图:共享数据并隔离游标
slice 和 duplicate 共享底层数据但有独立游标;视图变化会影响源 Buffer 的内容,线程共享时尤其要明确所有权。
import java.nio.ByteBuffer;
public class BufferViewDemo {
public static void main(String[] args) {
ByteBuffer source = ByteBuffer.wrap(new byte[]{1, 2, 3});
source.position(1);
// 关键变化:source.position(1);;调用 position,实参为 1;将 source 的 position 设置为 1;位置状态更新。
ByteBuffer slice = source.slice();
slice.put(0, (byte) 9);
// 关键变化:slice.put(0, (byte) 9);;按索引 0 向 slice 写入 (byte) 9;绝对写入不改变 position。
System.out.println(source.get(1));
// 输出:9
}
}asReadOnlyBuffer:只读视图
只读视图防止通过该引用修改内容,但不能阻止源 Buffer 修改底层数组;需要真正隔离时复制数据。
import java.nio.ByteBuffer;
public class ReadOnlyBufferDemo {
public static void main(String[] args) {
ByteBuffer readOnly = ByteBuffer.wrap(new byte[]{1}).asReadOnlyBuffer();
System.out.println(readOnly.isReadOnly());
// 输出:true
}
}分散与聚集 I/O:在多个缓冲区间传输
Scatter/gather 适合固定头部加主体等协议格式;Buffer 数组的顺序、剩余量和部分写入都要由调用方管理。
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class ScatterGatherDemo {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("scatter-gather-", ".bin");
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
// 输入:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {;资源变量 channel 接收 FileChannel.open(file, StandardOpenOption.WRITE),try 结束时自动关闭。
// 关键变化:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {;创建资源 channel,构造表达式为 FileChannel.open(file, StandardOpenOption.WRITE);try 结束时关闭该资源。
long count = channel.write(new ByteBuffer[]{ByteBuffer.wrap(new byte[]{1}), ByteBuffer.wrap(new byte[]{2})});
// 关键变化:long count = channel.write(new ByteBuffer[]{ByteBuffer.wrap(new byte[]{1}), ByteBuffer.wrap(new byte[]{2})});;按顺序写入两个各含 1 字节的 ByteBuffer,返回写入字节总数 2 并写入 count。
System.out.println(count);
// 输出:2
}
Files.deleteIfExists(file);
// 关键变化:Files.deleteIfExists(file);;删除路径 file;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}AsynchronousFileChannel:异步文件操作
异步 Channel 的 completion handler/future 让等待方式不同,但不代表磁盘本身一定并行;需要结合线程池、队列和取消策略测量。
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class AsyncFileChannelDemo {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("async-channel-", ".txt");
Files.writeString(file, "java");
// 关键变化:Files.writeString(file, "java");;将 "java" 写入路径 file;writeString 返回目标 Path,文件内容更新。
try (var channel = AsynchronousFileChannel.open(file, StandardOpenOption.READ)) {
// 输入:try (var channel = AsynchronousFileChannel.open(file, StandardOpenOption.READ)) {;资源变量 channel 接收 AsynchronousFileChannel.open(file, StandardOpenOption.READ),try 结束时自动关闭。
// 关键变化:try (var channel = AsynchronousFileChannel.open(file, StandardOpenOption.READ)) {;创建资源 channel,构造表达式为 AsynchronousFileChannel.open(file, StandardOpenOption.READ);try 结束时关闭该资源。
var result = channel.read(ByteBuffer.allocate(4), 0).get();
// 关键变化:var result = channel.read(ByteBuffer.allocate(4), 0).get();;异步读取从位置 0 开始的 4 个字节,get 等待完成并把读取字节数 4 写入 result。
System.out.println(result);
// 输出:4
}
Files.deleteIfExists(file);
// 关键变化:Files.deleteIfExists(file);;删除路径 file;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}简单案例
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class BufferChannelDemo {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("buffer-channel-", ".txt");
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
// 输入:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE, StandardOpenOption.READ)) {;资源变量 channel 接收 FileChannel.open(file, StandardOpenOption.WRITE, StandardOpenOption.READ),try 结束时自动关闭。
// 关键变化:try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE, StandardOpenOption.READ)) {;创建资源 channel,构造表达式为 FileChannel.open(file, StandardOpenOption.WRITE, StandardOpenOption.READ);try 结束时关闭该资源。
ByteBuffer write = ByteBuffer.wrap("NIO".getBytes());
while (write.hasRemaining()) {
channel.write(write);
// 关键变化:channel.write(write);;向 channel 写入 write 的剩余字节,返回本次写入字节数;write position 前进。
}
channel.position(0);
// 关键变化:channel.position(0);;调用 position,实参为 0;将 channel 的 position 设置为 0;位置状态更新。
ByteBuffer read = ByteBuffer.allocate(3);
channel.read(read);
// 关键变化:channel.read(read);;从文件读取 3 个字节“NIO”到 read,返回 3;EOF 时才返回 -1。
read.flip();
// 关键变化:read.flip();;调用 flip,实参为 无显式参数;将 read 切换到读模式,position 置 0、limit 设为此前写入长度;缓冲状态更新。
System.out.println((char) read.get() + "" + (char) read.get() + (char) read.get());
// 输出:NIO
}
Files.deleteIfExists(file);
// 关键变化:Files.deleteIfExists(file);;删除路径 file;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}案例体现了写 Buffer、循环写入、移动 Channel position、读入和 flip 的完整状态流转。
易混点
capacity是总容量,limit是当前有效边界,position是下一次读写位置。flip切换到读取已有数据,clear准备覆盖,compact保留未读数据。- Channel 的
read/write都可能部分完成,不能把一次调用当作全部数据。 - Selector 适合非阻塞网络,不会让普通 FileChannel 获得网络式事件循环。
课后小问
为什么写入 ByteBuffer 后直接
get()容易读不到数据? 答案:写入后 position 在数据末尾,若不 flip,读模式的有效区间仍不正确。 解析:flip会把已写长度设为 limit,并把 position 归零,随后才能读取。为什么 FileChannel.write 要循环调用? 答案:一次 write 只保证尽力消费当前 Buffer,返回值可能小于剩余字节数。 解析:循环检查
hasRemaining()才能保证整个 Buffer 被写出,网络 Channel 更应如此。
本节小结
- Buffer 用 position、limit、capacity 描述读写状态,状态切换决定数据是否可见。
- Channel 负责交换数据,读写都要处理部分完成和 EOF。
- FileChannel 支持随机访问、通道传输和映射,但复杂度随能力增加。
- Selector、异步文件和 Direct Buffer 只在明确的性能/并发边界中使用。
快速回顾
- 写后读通常是
put -> flip -> get。 - 复用 Buffer:丢弃旧数据用
clear,保留未读数据用compact。 - FileChannel 读写要循环,transferTo 也要考虑部分传输。
- Selector 管网络事件,Files/阻塞 Channel 适合多数普通文件任务。
阿源的知识库