文章目录
1. NIO概念
Java NIO(New IO)是从Java 1.4版本开始引入的 一个新的IO API,可以替代标准的Java IO API。 NIO与原来的IO有同样的作用和目的,但是使用 的方式完全不同,NIO支持面向缓冲区的、基于 通道的IO操作。NIO将以更加高效的方式进行文 件的读写操作。
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
(无) | 选择器(Selectors) |
2. 缓冲区
Java NIO系统的核心在于:通道(Channel)和缓冲区 (Buffer)。通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
2.1 缓冲区类型
Buffer 底层维护了一个数组,可以保存多个相同类型的数据。根
据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
上述 Buffer 类 他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。
2.2 缓冲区基本属性
容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据 不可读写。缓冲区的限制不能为负,并且不能大于其容量。
位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。
标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这 个 position。
2.3 Buffer常用方法
方法 | 描述 |
---|---|
Buffer clear() | 清空缓冲区并返回对缓冲区的引用 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置充值为 0 |
int capacity() | 返回 Buffer 的 capacity 大小 |
boolean hasRemaining() | 判断缓冲区中是否还有元素 |
int limit() | 返回 Buffer 的界限(limit) 的位置 |
Buffer limit(int n) | 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象 |
Buffer mark() | 对缓冲区设置标记 |
int position() | 返回缓冲区的当前位置 position |
Buffer position(int n) | 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象 |
int remaining() | 返回 position 和 limit 之间的元素个数 |
Buffer reset() | 将位置 position 转到以前设置的 mark 所在的位置 |
Buffer rewind() | 将位置设为为 0, 取消设置的 mark |
2.4 缓冲区的数据操作
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)
2.5 直接与非直接缓冲区
ByteBuffer是一个抽象类,HeapByteBuffer和DirectByteBuffer,即字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则Java虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后), 虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。详情见《进阶篇》中的MMAP和零拷贝。
非直接缓冲区即我们常见的堆内存,使用java.nio.ByteBuffer#allocate
进行申请,源码如下:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
可以看到,申请堆内存实际上就是申请字节数组。
直接字节缓冲区可以通过调用此类的java.nio.ByteBuffer#allocateDirect
方法来创建。此方法返回的缓冲区进行分配和取消 分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。FileChannel的map()方法将文件区域直接映射到内存中来创建。该方法返回抽象类 MappedByteBuffer,它的实现类是DirectByteBuffer,即堆外内存。虽然堆外内存不受JVM管理,但是JAVA代码(堆中)中可以持有堆外内存的引用,如上述MappedByteBuffer对象。源码如下:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
//如果是按页对齐,则还要加一个Page的大小;我们分析只pa为false的情况就好了
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//预分配内存
Bits.reserveMemory(size, cap);
long base = 0;
try {
//分配内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
//将分配的内存的所有值赋值为0
unsafe.setMemory(base, size, (byte) 0);
//为address赋值,address就是分配内存的起始地址,之后的数据读写都是以它作为基准
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
//pa为false的情况,address==base
address = base;
}
//创建一个Cleaner,将this和一个Deallocator对象传进去
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
可以看到,申请直接缓冲区时,调用了native方法Unsafe#allocateMemory
,关于Cleaner和堆外内存的垃圾回收,请看参考《进阶篇》。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。
3. 通道
通道(Channel):由 java.nio.channels包定义的。Channel 表示 IO 源与目标打开的连接,如socketChannel代表TCP连接,DatagramChannel代表UDP连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
3.1 通道类型
Java 为 Channel 接口提供的最主要实现类如下:
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
3.2 获取通道
-
Java 针对支持通道的类提供了一个 getChannel() 方法。
本地IO操作-
FileInputStream/File Output Stream
-
RandomAccessFile
网络IO
-
Socket
-
ServerSocket
-
DatagramSocket
-
-
在JDK1.7中的NIO.2 针对各个通道提供了静态方法open();
-
在JDK1.7中的NIO.2 的Files类的静态方法newByteChannel();
3.3 通道的数据传输
将 Buffer 中数据写入 Channel
int byteWritten = inChannel.write(buf)
从 Channel 读取数据到 Buffer 例如:
int byteRead = inChannel.read(buf)
4. NIO的非阻塞式网络通信
-
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不 能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理, 当服务器端需要处理大量客户端时,性能急剧下降。
-
Java NIO是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同 时处理连接到服务器端的所有客户端。
4.1 Selector
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
selector的应用步骤如下:
- 创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
Selector selector = Selector.open()
- 向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
// 创建一个socket套接字
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9898)
// 获取SocketChannel
SocketChannel channel = socket.getChannel();
// 创建选择器
Selector selector = Selector.open();
// 将SelectorChannel切换到非阻塞模式
channel.configureBlocking(false);
// 想selector注册channel
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
常用方法如下:
方法 | 描述 |
---|---|
Set keys() | 所有的 SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() | 被选择的 SelectionKey 集合。返回此Selector的已选择键集 |
int select() | 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时, 该方法返回,并将对应的 SelectionKey 加入被选择的 SelectionKey 集合中,该方法返回这些 Channel 的数量。 |
int select(long timeout) | 可以设置超时时长的 select() 操作 |
int selectNow() | 执行一个立即返回的 select() 操作,该方法不会阻塞线程 |
Selector wakeup() | 使一个还未返回的 select() 方法立即返回 |
void close() | 关闭该选择器 |
4.2 SelectionKey
SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向 选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整 数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操 作。
可以监听的事件类型(可使用 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;
详情请参考《进阶篇》中位运算符的相关介绍。
常用方法如下:
方法 | 描述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
4.3 示例
@Test
public void client() throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",7498));
// 切换成非 阻塞模式
socketChannel.configureBlocking(false);
FileChannel inputChannel = FileChannel.open(Paths.get("/Users/djg/Downloads/branch.jpg"), StandardOpenOption.READ);
ByteBuffer clientBuffer = ByteBuffer.allocate(1024);
while (inputChannel.read(clientBuffer) != -1){
clientBuffer.flip();
socketChannel.write(clientBuffer);
clientBuffer.clear();
}
socketChannel.close();
inputChannel.close();
}
@Test
public void server() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 非阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(7498));
FileChannel outputChannel = FileChannel.open(Paths.get("/Users/djg/Downloads/branch2.jpg"),StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 选择器
Selector selector = Selector.open();
// 将通道注册到选择器上,并制定监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 轮巡式获得选择器里的已经准备就绪的事件
while (selector.select() > 0 ){
// 获取已经就绪的监听事件
Iterator<SelectionKey> selectorIterator = selector.selectedKeys().iterator();
// 迭代获取
while (selectorIterator.hasNext()){
// 获取准备就绪的事件
SelectionKey key = selectorIterator.next();
SocketChannel socketChannel = null;
// 判断是什么事件
if (key.isAcceptable()){
// 或接受就绪,,则获取客户端连接
socketChannel = serverSocketChannel.accept();
//切换非阻塞方式
socketChannel.configureBlocking(false);
// 注册到选择器上
socketChannel.register(selector,SelectionKey.OP_READ);
} else if (key.isReadable()){
// 获取读就绪通道
SocketChannel readChannel = (SocketChannel) key.channel();
readChannel.configureBlocking(false);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int len = 0;
while ( (len = readChannel.read(readBuffer)) != -1){
readBuffer.flip();
System.out.println(new String(readBuffer.array(),0,len));
outputChannel.write(readBuffer);
readBuffer.clear();
}
readChannel.close();
outputChannel.close();
}
}
// 取消选择键
selectorIterator.remove();
}
}
想要进一步了解NIO的相关知识如MMAP、堆外内存等,请关注《进阶篇》。