Java NIO是从Java1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作
IO | NIO |
---|---|
面向流 | 面向缓冲区 |
阻塞IO | 非阻塞IO |
选择器 |
Java NIO 系统的核心在于:通道和缓冲区。
通道表示打开到 IO 设备的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
缓冲区:一个用于特定基本数据类型的容器,由java.nio包定义的,所有缓冲区都是Buffer抽象类的子类。
Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
Buffer就像一个数组,可以保存多个相同类型的数据,根据数据类型不同(boolean除外),有以下Buffer常用子类:
上述 Buffer 类,他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个容量为capacity的Buffer对象
static XxxBuffer allocate(int capacity)
Buffer中的重要概念
0 <= mark <= position <= limit <= capacity
方法 | 描述 |
---|---|
Buffer clear() | 清空缓冲区并返回对缓冲区的引用 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置重置为0 |
int capacity() | 返回Buffer 的 capacity |
int limit() | 返回Buffer 的 limit |
int position() | 返回Buffer 的 position |
Buffer limit(n) | 设置缓冲区的limit为n,并返回修改后的Buffer对象 |
Buffer position(n) | 设置缓冲区的position为n,并返回修改后的Buffer对象 |
bool hasRemaining() | 判断缓冲区中是否还有元素 |
int remaining() | 返回position 和 limit 之间的元素个数 |
Buffer mark() | 对缓冲区设置标记 |
Buffer reset() | 将位置 position 转到以前设置的 mark 所在的位置 |
Buffer rewind() | 将位置设为0,取消设置的 mark |
Buffer 为所有子类提供了两个用于数据操作的方法:get() 和 put() 方法
获取Buffer中的数据
get(): 读取单个字节
get(byte[] dst): 批量读取多个字节到dst中
get(int index): 读取指定索引位置的字节(不会移动position)
放入数据到Buffer中
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。
通道:由java.nio.channels包定义的,Channel表示IO源与目标打开的连接,Channel类似于传统的”流”,只不过Channel本身不能直接访问数据,Channel只能与Buffer 进行交互。
DMA方式,仍然需要CPU介入

通道方式,通道本身也是处理器,可以独立处理IO操作

Java为 Channel 接口提供的最主要的实现类如下:
对支持通道的对象调用 getChannel() 方法。支持通道的类如下:
使用Files类的静态方法 newByteChannel() 获取字节通道
通过通道的静态方法open()打开并返回指定通道
将 Buffer 中数据写入 Channel
int bytesWritten = outChannel.write(buf);
从Channel中读取数据到Buffer中
int bytesRead = inChannel.read(buf);
例:文件复制,将已存在的1.png图片复制到文件2.png
@Test
public void Test1() throws IOException {
FileInputStream fileInputStream = new FileInputStream("1.png");
FileOutputStream fileOutputStream = new FileOutputStream("2.png");
// 获取通道
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
// 分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将通道中的数据写入缓冲区中 inChannel ---> byteBuffer (此时写数据到缓冲区)
while(inChannel.read(byteBuffer) != -1) {
// 将缓冲区切换成读数据模式
byteBuffer.flip();
// byteBuffer ---> outChannel (此时从缓冲区读数据)
outChannel.write(byteBuffer);
byteBuffer.clear();
}
outChannel.close();
inChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
分散读取:指从Channel中读取的数据“分散”到多个Buffer中

聚集写入:指将多个 Buffer 中的数据“聚集”到Channel

?
例:
将数据从源通道传输到其他通道中,transferFrom() 方法
public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;
inChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("3.png"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE_NEW);
outChannel.transferFrom(inChannel, 0, inChannel.size());
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
inChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("3.png"), StandardOpenOption.WRITE,StandardOpenOption.READ, StandardOpenOption.CREATE_NEW);
inChannel.transferTo(0, inChannel.size(), outChannel);
方法 | 描述 |
---|---|
int read(ByteBuffer dst) | 从Channel中读取数据到ByteBuffer |
Long read(ByteBuffer[] dsts) | 将Channel中的数据“分散“到ByteBuffer[] |
int write(ByteBuffer src) | 将ByteBuffer中的数据写入到Channel |
long write(ByteBuffer[] srcs) | 将ByteBuffer[]中的数据”聚集“到Channel中 |
Long position() | 返回此通道的文件位置 |
FileChannel position(long p) | 设置此通道的文件位置 |
long size() | 返回此通道的文件的当前大小 |
FileChannel truncate(long s) | 将此通道的文件截取为给定大小 |
void force(boolean metaData) | 强制将所有对此通道的文件更新写入到存储设备中 |
选择器是SelectableChannel对象的多路复用器,Selector可以同时监控对个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心
SelectableChannel的结构如下:
创建Selector:通过调用Selector.open()方法创建一个Selector。
Selector selector = Selector.open();
向选择器注册通道
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
当调用register(Selector sel, int ops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定
可以监听的事件类型(可使用SelectionKey的四个常量表示):
读 : SelectionKey.OP_READ (1)
写 : SelectionKey.OP_WRITE (4)
连接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
当注册时不止监听一个事件,则可以用”位或“操作符连接
// 注册监听事件
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
Selector的常用方法
方法 | 描述 |
---|---|
Set |
返回注册在该Selector上的所有的 SelectionKey 集合 |
selectedKeys() | 返回注册在该Selector上的已被选择的 SelectionKey 集合 |
int select() | 监控所有注册的Channel,返回需要被处理的Channel数量 |
int select(long timeout) | 可设置超时时长的select()操作 |
Selector wakeup() | 使一个还未返回的select()方法立即返回 |
void close() | 关闭该选择器 |
SelectionKey: 表示SelectableChannel 和 Selector之间的注册关系。每次选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集,操作集的每一位都表示该键的通道所支持的一类可选择操作。
方法
方法 | 描述 |
---|---|
Int interestsQps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
Boolean isReadable() | 检测 Channel 中读事件是否就绪 |
Boolean isWriteable() | 检测 Channel 中写事件是否就绪 |
Boolean isConnectable() | 检测 Channel 中连接是否就绪 |
Boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
例:模拟客户端和服务器通信
public class TestNonBlockingNIO {
@Test
public void client() throws IOException {
// 1.获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
// 2.切换非阻塞模式
socketChannel.configureBlocking(false);
// 3.分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 4.发送数据给服务端
byteBuffer.put(new Date().toString().getBytes());
byteBuffer.put(scanner.next().getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
// 5.关闭通道
socketChannel.close();
}
@Test
public void server() throws IOException {
// 1.获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2.切换非阻塞模式
serverSocketChannel.configureBlocking(false);
// 3.绑定连接
serverSocketChannel.bind(new InetSocketAddress(9898));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器,并且指定"监听接收事件"
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6.轮询式地获取选择器上已经"准备就绪"的事件
while (selector.select() > 0) {
// 7.获取当前选择器中所有注册的"选择键(已就绪的监听事件)"
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
// 8.获取准备就绪的事件
SelectionKey selectionKey = iterator.next();
// 9.判断具体是什么事件准备就绪
if (selectionKey.isAcceptable()) {
// 10.若接收就绪,就获取客户端的连接
SocketChannel socketChannel = serverSocketChannel.accept();
// 11.切换非阻塞模式
socketChannel.configureBlocking(false);
// 12.将该通道注册到选择器上,并且指定"监听读就绪事件"
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 13.获取当前选择器上"读就绪"状态的通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 14.读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = socketChannel.read(byteBuffer)) > 0) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
}
// 15.取消选择键selectionKey
iterator.remove();
}
}
}
}
? Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

例:向管道中写数据后读出:
@Test
public void test1() throws IOException {
// 1.获取管道
Pipe pipe = Pipe.open();
// 2.将缓冲区中的数据写入管道
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel = pipe.sink();
byteBuffer.put("通过单向管道发送数据".getBytes());
byteBuffer.flip();
sinkChannel.write(byteBuffer);
byteBuffer.clear();
// 3.将管道中的数据写入缓冲区
Pipe.SourceChannel sourceChannel = pipe.source();
sourceChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
}
? 随着 JDK 7 的发布,Java对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2。因为 NIO 提供的一些功能,NIO已经成为文件处理中越来越重要的部分。
方法 | 描述 |
---|---|
boolean endsWith(String path) | 判断是否以path路径结束 |
boolean startsWith(String path) | 判断是否以path路径开始 |
boolean isAbsolute() | 判断是否是绝对路径 |
Path getFileName() | 返回与调用Path对象关联的文件名 |
Path getName(int index) | 返回指定所用位置index的路径名称 |
int getNameCount() | 返回Path根目录后面元素的数量 |
Path getParent() | 返回Path对象包含整个路径,不包含 Path 对象指定的文件路径 |
Path getRoot() | 返回调用 Path 对象的根路径 |
Path resolve(Path p) | 将相对路径解析为绝对路径 |
Path toAbsolutePath() | 作为绝对路径返回调用 Path 对象 |
String toString() | 返回调用 Path 对象的字符串表示形式 |
Java.nio.file.Files用于操作文件或目录的工具类
Files常用方法
方法 | 描述 |
---|---|
Path copy(Path src, Path dest, CopyOption ... how) | 文件的复制 |
Path createDirectory(Path path, FileAttribute<?> ...attr) | 创建一个目录 |
Path CreateFile(Path path, FileAttribute<?> … arr) | 创建一个文件 |
Void delete(Path path) | 删除一个文件 |
Path move(Path src, Path dst, CopyOption ...how) | 将src移动到dst位置 |
Path move(Path src, Path dest, CopyOption…how) | 将 src 移动到 dest 位置 |
long size(Path path) | 返回 path 指定文件的大小 |
原文:https://www.cnblogs.com/sky--blog/p/12748974.html