1.1 概述
Java NIO(New IO / Nonblock IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。 NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
与 IO 的主要区别
- 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理, 当服务器端需要处理大量客户端时,性能急剧下降。
- Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
(无) | 选择器(Selectors) |
核心
NIO的核心在于:通道(Channel)、缓冲区(Buffer)和选择器(Selector)。通道表示打开到IO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。简而言之,Channel 负责传输, Buffer 负责存储。
1.2 缓冲区(Buffer)
基本概念
- 一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是Buffer抽象类的子类。
- Java NIO中的Buffer主要用于与NIO通道进行 交互,数据从通道读入缓冲区,从缓冲区写入通道中。
- Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean除外) ,有以下Buffer常用子类:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
上述Buffer类都采用相似的方法进行管理数据。都可以通过如下方法获取一个 Buffer 对象:
static XxxBuffer allocate(int capacity)
: 创建一个容量为capacity 的 XxxBuffer 对象。
基本属性
- 容量 (capacity) :表示Buffer最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
- 限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
- 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。
- 标记 (mark)与重置 (reset):标记是一个索引,通过Buffer中的
mark()
方法指定Buffer中一个特定的position,之后可以通过调用reset()
方法恢复到这个position.
标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
-
示意图如下:
Buffer 基本属性
Buffer数据操作
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)
public class TestBuffer {
@Test
public void test2() {
String str = "abcdefgh";
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(str.getBytes());
byteBuffer.flip();
byte[] temp = new byte[byteBuffer.limit()];
byteBuffer.get(temp, 0, 2);
System.out.println(new String(temp));
System.out.println(byteBuffer.position());
byteBuffer.mark();
byteBuffer.get(temp, 2, 2);
System.out.println(new String(temp));
System.out.println(byteBuffer.position());
byteBuffer.reset();
System.out.println(byteBuffer.position());
byteBuffer.get(temp, 4, 2);
System.out.println(new String(temp));
System.out.println(byteBuffer.position());
//判断缓冲区中是否还有剩余数据
if(byteBuffer.hasRemaining()){
//获取缓冲区中可以操作的数量
System.out.println(byteBuffer.remaining());
}
}
}
输出结果:
ab
2
abcd
4
2
abcdcd
4
4
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 |
public class TestBuffer {
@Test
public void test1() {
String str = "abcdefg";
//分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("----------------allocate()------------");
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());
//2. 利用 put() 存入数据到缓冲区中
byteBuffer.put(str.getBytes());
System.out.println("----------------put()------------");
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());
//3. 切换读取数据模式
byteBuffer.flip();
System.out.println("----------------flip()------------");
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());
//4. 利用 get() 读取缓冲区中的数据
byte[] temp = new byte[byteBuffer.limit()];
byteBuffer.get(temp);
System.out.println(new String(temp, 0, temp.length));
System.out.println("----------------get()------------");
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());
//5. rewind() : 可重复读
byteBuffer.rewind();
System.out.println("----------------rewind()------------");
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());
//6. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态
byteBuffer.clear();
System.out.println("----------------clear()------------");
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());
System.out.println((char)byteBuffer.get());
}
}
输出结果:
----------------allocate()------------
0
1024
1024
----------------put()------------
7
1024
1024
----------------flip()------------
0
7
1024
abcdefg
----------------get()------------
7
7
1024
----------------rewind()------------
0
7
1024
----------------clear()------------
0
1024
1024
a
直接与非直接缓冲区
- 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在 此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后), 虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
- 直接字节缓冲区可以通过调用此类的
allocateDirect()
工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。 - 直接字节缓冲区还可以通过
FileChannel
的map()
方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer
。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。 - 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其
isDirect()
方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。
非直接缓冲区
直接缓冲区
1.3 通道(Channel)
-
由 java.nio.channels 包定义。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
通道 - Java 为 Channel 接口提供的最主要实现类如下:
FileChannel
:用于读取、写入、映射和操作文件的通道。
DatagramChannel
:通过UDP读写网络中的数据通道。
SocketChannel
:通过TCP读写网络中的数据。
ServerSocketChannel
:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel
。
获取通道
- 获取通道的一种方式是对支持通道的对象调用
getChannel() 方法。支持通道的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
- 获取通道的其他方式是使用 Files 类的静态方法
newByteChannel()
获取字节通道。 - 或者通过通道的静态方法
open()
打开并返回指定通道。
通道的数据传输
- 将 Buffer 中数据写入 Channel
- 从 Channel 读取数据到 Buffer
public class TestChannel throws IOException {
@Test
public void test1(){
FileInputStream fis = null;
FileOutputStream fos = null;
//获取通道
FileChannel inChannel = null;
FileChannel outChannel = null;
fis = new FileInputStream("d:/1.mkv");
fos = new FileOutputStream("d:/2.mkv");
inChannel = fis.getChannel();
outChannel = fos.getChannel();
//分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//将通道中的数据存入缓冲区中
while(inChannel.read(buf) != -1){
buf.flip(); //切换读取数据的模式
//将缓冲区中的数据写入通道中
outChannel.write(buf);
buf.clear(); //清空缓冲区
}
outChannel.close();
inChannel.close();
fos.close();
fis.close();
}
}
}
- 将数据从源通道传输到其他 Channel 中(
transferTo
、transferFrom
)
public class TestChannel {
@Test
public void test3() throws IOException{
FileChannel inChannel = FileChannel.open(Paths.get("d:/1.mkv"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:/2.mkv"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//通道间的数据传输
// inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
}
- 使用直接缓冲区完成文件的复制(内存映射文件)
public class TestChannel {
@Test
public void test2() throws IOException{
FileChannel inChannel = FileChannel.open(Paths.get("d:/1.mkv"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:/2.mkv"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//内存映射文件
MappedByteBuffer inMappedBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMappedBuf.limit()];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
inChannel.close();
outChannel.close();
}
}
分散(Scatter)和聚集(Gather)
-
分散读取(Scattering Reads)是指从 Channel 中读取的数据“分散”到多个 Buffer 中(按照缓冲区的顺序,从 Channel 中读取的数据依次将Buffer 填满)。
分散读取 -
聚集写入(Gathering Writes)是指将多个 Buffer 中的数据“聚集” 到 Channel(按照缓冲区的顺序,写入 position 和limit 之间的数据到 Channel )。
聚集写入
public class TestChannel {
@Test
public void test4() throws IOException{
RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
//1. 获取通道
FileChannel channel1 = raf1.getChannel();
//2. 分配指定大小的缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//3. 分散读取
ByteBuffer[] bufs = {buf1, buf2};
channel1.read(bufs);
for (ByteBuffer byteBuffer : bufs) {
byteBuffer.flip();
}
//4. 聚集写入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
}
}
FileChannel 的常用方法
方法 | 描述 |
---|---|
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) | 强制将所有对此通道的文件更新写入到存储设备中 |
1.4 选择器(Selector)
- 选择器(Selector)是
SelectableChannel
对象的多路复用器,Selector 可以同时监控多个SelectableChannel
的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞的核心。 -
SelectableChannel
的结构:
SelectableChannle 的结构
Selector的应用
- 创建选择器:通过调用
Selector.open()
方法创建一个Selector; - 向选择器注册通道:
SelectableChannel.register(Selector sel, int ops)
public class TestNonBlockingNIO {
//客户端
@Test
public void client() throws IOException{
//1. 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2. 切换非阻塞模式
sChannel.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4. 发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
//5. 关闭通道
sChannel.close();
}
//服务端
@Test
public void server() throws IOException{
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6. 轮询式的获取选择器上已经“准备就绪”的事件
while(selector.select() > 0){
//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator it = selector.selectedKeys().iterator();
while(it.hasNext()){
//8. 获取准备“就绪”的是事件
SelectionKey sk = it.next();
//9. 判断具体是什么事件准备就绪
if(sk.isAcceptable()){
//10. 若“接收就绪”,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11. 切换非阻塞模式
sChannel.configureBlocking(false);
//12. 将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable()){
//13. 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = sChannel.read(buf)) > 0 ){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
}
}
注册选择器
- 当调用
register(Selector sel, int ops)
注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。 -
SelectionKey
:表示SelectableChannel
和Selector
之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。 - 可以监听的事件类型(可使用
SelectionKey
的四个常量表示):
- 读:
SelectionKey.OP_READ
- 写:
SelectionKey.OP_WRITE
- 连接:
SelectionKey.OP_CONNECT
- 接收:
SelectionKey.OP_ACCEPT
- 若注册时不止监听一个事件,则可以使用“位或”操作符连接。
常用方法
方法 | 描述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
Set |
所有的 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() | 关闭该选择器 |
1.5 管道(Pipe)
Java NIO 管道是2个线程之间的单向数据连接。 Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。