NIO
NIO早在JDK1.4中就已经提出来了(JSR51),在JDK1.7中对NIO进行了补充类库NIO.2(JSR 203),NIO又叫Non-blocking IO,即非阻塞IO
同步非阻塞
阻塞与非阻塞的区别:
阻塞时,在调用结果返回时,当前线程会被挂起,并在得到结果之后返回
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降
非阻塞时,如不能立即得到结果,该调用不会阻塞当前线程,可以继续完成其他操作,只需要定时轮询查看处理状态
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端
与普通IO的不同和关系
NIO是面向通道和缓冲区的,普通IO是面向字节流和字符流的
NIO不再是和IO一样用OutputStream和InputStream输入流的形式来进行处理数据的,但是又是基于这种流的方式,采用了通道和缓冲区的形式进行处理
NIO的通道是可以双向的,IO的流只能是单向的
NIO的缓冲区(字节数组)还可以进行分片,可以建立只读缓冲区、直接缓冲区和间接缓冲区,只读缓冲区就是只可以读,直接缓冲区是为了加快I/O速度,以一种特殊的方式分配其内存的缓冲区
NIO采用的是多路复用的IO模型,BIO用的是阻塞的IO模型
通道Channel负责传输,缓冲区Buffer负责存储
核心组件
NIO中包含有几个核心组件:Channel、Buffer、Selector
缓冲区Buffer
Buffer本质是一块内存区,可以来进行数据读取
Buffer是一个对象,它包含一些要写入或者刚读出的数据。在NIO中加入Buffer对象,在流式IO中,将数据直接写入或者读到Stream对象中
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区的。任何时候访问NIO中的数据,都需要将它放到缓冲区中
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程
1 | ByteBuffer |
重要变量及方法
最最重要的就是三个变量position、limit、capacity,这三个一定要弄清楚
1 | // 0<=mark<=position<=limit<=capacity |
示例:
1 | // 分配一个指定大小的缓冲区 |
步骤
声明buffer,分配一个指定大小的缓冲区,此时limit和capacity都已确定,为1024
1
ByteBuffer buf = ByteBuffer.allocate(1024);
将数据写入buffer,position会根据写入数据的长度而变化,即记录写入的位置
1
buf.put("hello".getBytes());
调用flip,将写模式切换为读模式,此时limit会变为写入的position值,position置为0,将原来的写模式切换为读模式
1
buf.flip();
从buffer中读取数据,由于切换为读模式时,limit为写入的position值,所以使用buf.limit就可以获取到写入的数据长度,读取时会根据读的数据来更新position值,标记读取的位置
1
2
3byte[] read = new byte[buf.limit()];
buf.get(read);
System.out.println(new String(read,0,read.length));调用buffer.clear,读取完成后,清除掉buffer中的数据,调用clear方法当然其实数据还是在的,只是将limit和position的值变为了初始化时的最原始的值
1
buf.clear();
直接缓冲区
1 | // 非直接缓冲区 |
通道Channel
通道是对原I/O包中的流的模拟,表示打开到IO设备的连接,本身不存储数据。NIO中的所有的IO操作都是从Channel开始的,到任何目的地的所有数据都必须通过一个Channel对象(通道),然后再将数据写到Buffer,一个Buffer实质上就是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;从通道中读取的任何数据都要读到缓冲区中,Channel只能与Buffer进行交互
与IO流的区别
- 通道可以读也可以写,而流只能是单向的(只能读或者写)
- 通道可以异步读写
- 通道总是基于缓冲区Buffer来进行读写的
1 | public interface Channel extends Closeable { |
该Channel基础接口有两个子接口ReadableByteChannel、WritableByteChannel,如果只实现两个接口中的任意一个那就是单向的,只能读或者只能写;如果同时实现了这两个接口,那就是双向的。当然这些接口基本上也不会用到,都被封装了很多层了
有两个比较重要的通道,文件通道和套接字通道
文件通道
FileChannel用于文件的数据读写,文件通道总是阻塞式的
FileChannel的创建
主要有两个方式
第一种方式
使用FileChannel.open()方法创建
1 | public static FileChannel open(Path path, OpenOption... options) |
1 | // 第一个参数为文件路径 |
第二种方式
使用文件流来创建
1 | FileChannel fileChannel = new FileOutputStream(FILE).getChannel() |
读写示例
1 | public static void main(String[] args) { |
文件传输
可以直接使用transferFrom、transferTo传输方法来快速的传输数据
1 | // 从src源通道中的数据写入当前文件通道 |
套接字通道
在之前使用套接字时通常使用Socket和ServerSocket来进行网络编程,ServerSocket使用accept进行端口监听,但是使用accept时会处于阻塞状态,一直等待客户端程序的连接请求,严重影响到系统的性能和吞吐量
在NIO中提供了NetworkChannel接口来进行套接字通道,其有三个重要的实现SocketChannel、ServerSocketChannel、DatagramChannel
客户端、服务端分别使用SocketChannel和ServerSocketChannel来创建通道,其提高性能和吞吐量的核心在于多路复用器,也就是Selector,可以设置为非阻塞
1 | // 设置为非阻塞 |
ServerSocketChannel
其对应于ServerSocket类
1 | // 创建ServerSocketChannel |
SocketChannel
其对应于Socket类
1 | // 创建SocketChannel |
DatagramChannel
对应DatagramSocket类,用来进行UDP通信的
选择器Selector
Selector是多路复用器,管理着一个被注册的通道集合的信息和它们的事件状态,用于同时监测多个SelectableChannel 通道的事件状态以实现单线程可以操作多个通道的数据来实现异步I/O。
SelectionKey选择键中封装了特定的通道与特定的选择器的注册关系,通过SelectableChannel.register方法返回被提供一个表示这种注册关系的标记
在使用的时候需要将Channel注册到Selector上,这样就可以调用Selector#select方法来找到一个状态符合条件的Channel。
通过一个选择器来同时对多个套接字通道进行监听,当套接字通道有可用的事件的时候,通道改为可用状态,选择器就可以实现可用的状态。
1 | // 读操作 |
获取已注册的键
有很多方式来进行获取
1 | // 该方法可以获取到所有注册过的键,但是并不是所有注册过的键都有效 |
步骤
创建一个Selector
1
Selector selector = Selector.open();
将channel注册到Selector
1
2
3
4
5SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞
socketChannel.configureBlocking(false);
// 注册监听的事件,第二个参数就是上述所列出的事件,该方法的返回值是SelectionKey
socketChannel.register(selector, SelectionKey.OP_CONNECT);
取出所监听的事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 关注的事件集合
int interestSet = selectionKey.interestOps();
// 在使用的时候使用&来进行判断是否包含该事件
boolean isInterestAccept = (interestSet & SelectionKey.OP_ACCEPT) != 0;
boolean isInterestConnect = (interestSet & SelectionKey.OP_CONNECT) != 0;
boolean isInterestRead = (interestSet & SelectionKey.OP_READ) != 0;
boolean isInterestWrite = (interestSet & SelectionKey.OP_WRITE) != 0;
// 获取准备就绪的事件集合
int readySet = selectionKey.readyOps();
// 在使用的时候使用&来进行判断是否包含该事件
boolean isAccept = (readySet & SelectionKey.OP_ACCEPT) != 0;
boolean isConnect = (readySet & SelectionKey.OP_CONNECT) != 0;
boolean isRead = (readySet & SelectionKey.OP_READ) != 0;
boolean isWrite = (readySet & SelectionKey.OP_WRITE) != 0;
// 也可以直接使用方法判断
selectionKey.isReadable()
selectionKey.isWritable()
selectionKey.isConnectable()
selectionKey.isAcceptable()获取channel和selector
1
2
3
4// channel方法可以返回与该键相关的SelectableChannel对象
Channel channel = selectionKey.channel();
// selector返回相关的Selector对象
Selector selector = selectionKey.selector()调用select可以获取channel,该方法会进行阻塞,直到有合适的channel,返回值为1表示有channel达到就绪状态;已取消的键会被清理掉
1
selector.select();
获取事件集合
1
2Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();遍历事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if(selectionKey.isAcceptable()){
// doSomething
} else if(selectionKey.isReadable()){
// doSomething
} else if(selectionKey.isWritable()){
// doSomething
}
// Selector本身不会移除SelectKey,所以需要手动移除,不然会重复操作
// 当下次channel处于就绪状态时,Selector会把这些key再添加进来
iterator.remove();
}
工作原理
客户端——-》Channel——-》Selector———》keys—状态改变—-》server
- 当客户端发起连接时,会通过ServerSocketChannel创建对应的SocketChannel
- 调用SocketChannel的注册方法将SocketChannel注册到Selector上,会返回一个SelectionKey。通过SelectionKey可以找到对应的Selector,也可以找到对应的SocketChannel
- Selector通过调用select()方法对内部的SelectionKey集合所关联的SocketChannel集合进行监听
- 通过SelectionKey找到有事件发生的SocketChannel,完成数据处理
Buffer 缓冲区
Channel 通道
Selector 选择器
一个Selector对应一个处理线程
一个Selector可以注册多个Channel,Selector会根据不同的事件在各个Channel上切换
Server端创建ServerSocketChannel 有一个Selector多路复用器 轮询所有注册的通道,根据通道状态,执行相关操作
- Connect 连接状态
- Accept 阻塞状态
- Read 可读状态
- Write 可写状态
Client端创建SocketChannel 注册到Server端的Selector
NIO网络编程示例
1 | ByteBuffer byteBuffer = ByteBuffer.allocate(1024); |
内存映射文件
内存映射文件可以创建和修改那些因为太大而无法放入内存的文件,使用内存映射缓冲区MappedByteBuffer
使用FileChannel.map()方法可以在一个打开的文件和一个特殊类型的ByteBuffer之间建立一个虚拟内存映射,创建一个由磁盘文件支持的虚拟内存映射并在那块虚拟内存空间外部封装一个MappedByteBuffer对象
该MappedByteBuffer对象调用get方法会从磁盘文件中获取数据,调用put方法会更新磁盘上的文件,该操作比常规读写效率高,操作系统的虚拟内存可以自动缓存内存页,这些页是用系统内存来缓存的,不会消耗JVM堆内存;且访问该内存页不需要再次调用系统命令来获取数据
1 | RandomAccessFile tdat = new RandomAccessFile("test.dat", "rw"); |
映射文件访问比标准IO性能高很多
文件锁定
文件锁定可同步访问,文件锁对其他操作系统进程可见,因为java文件锁直接映射到本机操作系统锁定工具。
1 | public class FileLockTest { |
通过调用FileChannel上的tryLock或lock,可以获得整个文件的FileLock(SocketChannel、DatagramChannel和ServerSocketChannel不需要锁定,因为本质上就是单线程实体)
tryLock()是非阻塞的,试图获取锁,若不能获取,只是从方法调用返回
lock()会阻塞,直到获得锁,或者调用lock()的线程中断,或者调用lock()方法的通道关闭。
使用FileLock.release()释放锁
1 | // 锁定文件的一部分,锁住size-position区域。第三个参数指定是否共享此锁 |