java高级之NIO
BIO、NIO、AIO区别
-
BIO:
Block IO
同步阻塞式 IO,在传统的java.io
包下,它基于流模型实现(面向流的IO操作),提供了我们最熟知的一些IO功能,例如File 抽象、输入输出流等。BIO的交互方式是同步、阻塞的方式,即在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。 -
NIO:
non-blocking IO
同步非阻塞 IO,是在JDK1.4
中引入的NIO框架(java.nio
包),可以看作是传统IO的升级,NIO支持面向缓冲区的、基于通道的IO操作。NIO提供了Selector
、Channel
、Buffer
等新的抽象,可以构建多路复用的、同步非阻塞IO程序,提供了更接近操作系统底层的高性能数据操作方式。 -
AIO:
Asynchronous IO
是 NIO 的升级,在JDK1.7
中实现,也叫NIO2
,实现了异步非堵塞IO,异步IO的操作基于事件和回调机制。
NIO知识整理
- NIO主要有三大核心部分:Selector(选择器)、Channel(通道)、Buffer(缓冲区);
- NIO是面向缓冲区,面向块的编程,数据读取到一个稍后会处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用NIO可以提供非阻塞式的高伸缩性网络。
NIO与IO区别
IO(同步阻塞式 IO) | NIO(同步非阻塞 IO) |
---|---|
面向流 | 面向缓冲区 |
阻塞式 | 非阻塞式 |
选择器、通道 |
- IO是面向流的,流是单向的,比如从文件(磁盘、网络)到程序的过程中使用的输入输出流都是单向的。
- NIO是面向缓冲区的,NIO在文件(磁盘、网络)和程序之间建立通道(Channel),传输的数据通过缓冲区进行存取,缓冲区在通道中进行传递运输,例如火车与铁轨的关系,是双向的。
通道与缓冲区
- 通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
- Channel 负责传输、连接, Buffer 负责数据存储、操作。
- 缓冲区( Buffer):一个用于特定基本数据类型的容器。它主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
缓冲区Buffer
-
由
java.nio
包定义,所有缓冲区都是Buffer
抽象类的子类。常用子类如下: -
Buffer中的四个核心属性:
- 标记、位置、限制、容量遵守以下不变式:
0 <= mark <= position <= limit <= capacity
。
public abstract class Buffer { //标记 (mark)与重置 (reset): 标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position。 private int mark = -1; //下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制 private int position = 0; //限制 (limit): 第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。 private int limit; //容量 (capacity) : 表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。 private int capacity; }
- 标记、位置、限制、容量遵守以下不变式:
-
Buffer
的常用方法返回值 方 法 Buffer clear() 清空缓冲区并返回对缓冲区的引用 Buffer flip() 翻转缓冲区,读写切换 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 -
Buffer的子类常用方法,注意字节缓冲区中的直接缓冲区与非直接缓冲区的区别。
- 直接缓冲区可以通过
allocateDirect()
和FileChannel
的map()
方法来创建,返回值为MappedByteBuffer
。
方法的返回值 方法名 描述 static ****Buffe
allocate(int capacity)
分配一个新的**缓冲区。 static ByteBuffer
allocateDirect(int capacity)
分配一个新的直接字节缓冲区。 abstract ***
get()
获取缓冲区的数据,多种类型的重载 abstract ***Buffer
put(***)
存入数据到缓冲区 重载 - 直接缓冲区可以通过
-
代码练习
@Test public void testBufer(){ String str = "Practice Buffer"; //1、创建一个字节缓冲区 分配大小为128 ByteBuffer byteBuffer = ByteBuffer.allocate(128); //2、核心属性 0 <= mark <= position <= limit <= capacity // 2.1拿到容量 System.out.println("容量为:"+byteBuffer.capacity()); // 2.2拿到限制 System.out.println("限制为:"+byteBuffer.limit()); // 2.3拿到位置 System.out.println("当前位置为:"+byteBuffer.position()); //3、put() 将数据写入缓冲区 byteBuffer.put(str.getBytes()); //4、flip() 切换读写模式 byteBuffer.flip(); //5、get() 读取数据 byte[] bytes = new byte[byteBuffer.limit()]; byteBuffer.get(bytes); System.out.println(new String(bytes,0,bytes.length)); //6、rewind() 可重复读 将位置设为为 0 byteBuffer.rewind(); //7、读两个位置的数据 byte[] dst = new byte[byteBuffer.limit()]; byteBuffer.get(dst, 0, 2); System.out.println(new String(dst, 0, 2)); //拿到位置和限制 System.out.println("当前位置为:"+byteBuffer.position()+",当前限制为"+byteBuffer.limit()); //8、核心属性标记 mark byteBuffer.mark(); //9、在读两个字节的数据 byteBuffer.get(dst,2,2); //拿到位置和限制 System.out.println("当前位置为:"+byteBuffer.position()+",当前限制为"+byteBuffer.limit()); //10、reset() 恢复到 标记mark位置 byteBuffer.reset(); //拿到位置和限制 System.out.println("当前位置为:"+byteBuffer.position()+",当前限制为"+byteBuffer.limit()); //11、hasRemaining() 判断缓冲区是否还有元素 if(byteBuffer.hasRemaining()){ //12、可以操作的数量 返回 position 和 limit 之间的元素个数 System.out.println(byteBuffer.remaining()); } //取消设置的 mark byteBuffer.rewind(); //13、clear(); 清空缓冲区 但是缓冲区中数据仍然存在 byteBuffer.clear(); System.out.println((char)byteBuffer.get()); //14、分配直接缓冲区 ByteBuffer buf = ByteBuffer.allocateDirect(1024); //判断字节缓冲区是直接还是非直接 System.out.println(buf.isDirect()); }
通道(Channel)
-
在
java.nio.channels
包中定义,它表示 IO 源与目标打开的连接。可以将其类比于传统的“流”。但Channel
本身不能直接访问数据, 它只能与Buffer
进行交互。 -
获取通道
- 可以对支持通道的对象调用
getChannel()
方法。支持通道的类有:本地IO为FileInputStream
、FileOutputStream
、RandomAccessFile
,网络IO为DatagramSocket
、Socket
、ServerSocket
; - 在NIO2中,通过通道的静态方法
open()
打开并返回指定通道; - 在NIO2中,使用
Files
类的静态方法newByteChannel()
获取字节通道。
- 可以对支持通道的对象调用
/**
* 1、FileChannel 的open()方法 作用打开或创建文件,返回文件通道以访问该文件。
* 2、参数:path - 打开或创建文件的路径 options - 指定文件打开方式的选项
* 3、OpenOption 使用StandardOpenOption枚举类指定
* APPEND:如果文件打开 WRITE访问,则字节将被写入文件的末尾而不是开头。
* CREATE:创建一个新文件(如果不存在)。
* CREATE_NEW:创建一个新的文件,如果该文件已经存在失败。
* DELETE_ON_CLOSE:关闭时删除。
* DSYNC:要求将文件内容的每次更新都与底层存储设备同步写入。
* READ:打开阅读权限。
* SPARSE:稀疏文件
* SYNC:要求将文件内容或元数据的每次更新都同步写入底层存储设备。
* TRUNCATE_EXISTING:如果文件已经存在,并且打开 WRITE访问,则其长度将截断为0。
* WRITE:打开以进行写入。
*/
public static FileChannel open(Path path, OpenOption... options) throws IOException{
Set<OpenOption> set = new HashSet<OpenOption>(options.length);
Collections.addAll(set, options);
return open(path, set, NO_ATTRIBUTES);
}
-
可以利用通道完成整个数据传输,不使用缓冲区,使用通道的
transferFrom()
和transferTo()
方法。 -
通道的分散(
Scatter
)和聚集(Gather
):- 分散读取(
Scattering Reads
)是指从Channel
中读取的数据“分散” 到多个Buffer
中。 - 聚集写入(
Gathering Writes
)是指将多个Buffer
中的数据“聚集”到Channel
。
- 分散读取(
-
通道的常用方法
方 法 描 述 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) 强制将所有对此通道的文件更新写入到存储设备中 -
代码练习
//为了简化 未做异常处理
@Test
public void testChannel() throws IOException {
//一、使用非直接缓冲区完成文件复制
//1、创建文件输入流
FileInputStream fileInputStream = new FileInputStream("爱情与友情.jpg");
//2、创建文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("爱情与友情6.jpg");
//3、fileInputStream 与 fileOutputStream 支持通道 获取通道
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();
//4、分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//5、将通道中数据存入缓冲区
while (inputStreamChannel.read(byteBuffer)!=-1){
//6、切换读写模式
byteBuffer.flip();
//7、将数据写入到通道中
outputStreamChannel.write(byteBuffer);
//8、清空缓冲区
byteBuffer.clear();
}
//*********************************************************************************//
//二、使用直接缓冲区完成文件复制 通道的静态方法 open() 打开并返回指定通道
//1、创建通道
FileChannel inChannle = FileChannel.open(Paths.get("爱情与友情.jpg"), StandardOpenOption.READ);
FileChannel outChannle = FileChannel.open(Paths.get("爱情与友情7.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//2、直接缓冲区,内存映射文件
// 此频道文件的区域直接映射到内存中。 只读:READ_ONLY 读写:READ_WRITE 私有:PRIVATE
MappedByteBuffer inMappedByteBuffer = inChannle.map(FileChannel.MapMode.READ_ONLY, 0, inChannle.size());
MappedByteBuffer outMappedByteBuffer = outChannle.map(FileChannel.MapMode.READ_WRITE, 0, inChannle.size());
byte[] bytes = new byte[inMappedByteBuffer.limit()];
inMappedByteBuffer.get(bytes);
outMappedByteBuffer.put(bytes);
inChannle.close();
outChannle.close(); //*********************************************************************************//
//三、通道之间的数据传输
FileChannel inChannle1 = FileChannel.open(Paths.get("爱情与友情.jpg"), StandardOpenOption.READ);
FileChannel outChannle1 = FileChannel.open(Paths.get("爱情与友情8.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//inChannle1.transferTo(0,inChannle.size(),outChannle1);
outChannle1.transferFrom(inChannle1,0,inChannle1.size());
inChannle1.close();
outChannle1.close();
//*********************************************************************************//
//四、分散和聚集
//四-1 分散读取
//1、创建一个随机存取文件流
RandomAccessFile randomAccessFile = new RandomAccessFile("hello1.txt", "rw");
//2、获取通道
FileChannel channel = randomAccessFile.getChannel();
//3、获取缓冲区
ByteBuffer byteBuffer1 = ByteBuffer.allocate(10);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(100);
//4、分散读取
ByteBuffer[] byteBuffers = {byteBuffer1,byteBuffer2};
//5、将通道中数据分散到buffer中
channel.read(byteBuffers);
for (ByteBuffer byteBuffer3 : byteBuffers) {
//6、读写切换
byteBuffer3.flip();
}
//7、查看结果
System.out.println(new String(byteBuffers[0].array(), 0, byteBuffers[0].limit()));
System.out.println("===================================");
System.out.println(new String(byteBuffers[1].array(), 0, byteBuffers[1].limit()));
//四-2 聚集写入
RandomAccessFile raf2 = new RandomAccessFile("hello5.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(byteBuffers);
}
选择器
-
传统的阻塞IO方式在数据被读取或写入时,该线程在此期间不能执行其他任务。而NIO的非阻塞方式在没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
-
选择器
Selector
会不断地轮询注册在其上的Channel
,如果某个Channel
上面有新的TCP连接接入、读、写以及接收事件,这个Channel
就处于就绪状态,会被Selector
轮询出来,然后通过SelectionKey
可以获取就绪Channel
的集合,进行后续的I/O操作。也就是说选择器监控这些通道的IO状况(连接接入、读、写以及接收事件)。 -
选择器(
Selector
) 是SelectableChannle
对象的多路复用器,Selector
可以同时监控多个SelectableChannel
的 IO 状况,也就是说,利用Selector
可使一个单独的线程管理多个Channel
。Selector
是非阻塞 IO 的核心。 -
SelectableChannle
是可通过Selector
复用的通道,它是所有支持就绪检查的通道类的父类,提供了实现通道的可选择性所需要的公共方法。注意:FileChannel
类没有继承SelectableChannel
因此不是可选通道。 -
选择键(
SelectionKey
):选择键封装了特定的通道SelectableChannel
与特定的选择器Selector
的注册关系。选择键对象被SelectableChannel.register()
返回并提供一个表示这种注册关系的标记。 -
注册方法详解,第二个参数可以理解为选择器对通道的监听事件。多个监听事件时可以使用位或‘|’连接。
//sel - 要注册该频道的选择器 ops - 为结果键设置的兴趣 //SelectionKey中有四个事件:OP_CONNECT、OP_ACCEPT、OP_READ、OP_WRITE public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException
-
代码实例
- 阻塞式IO
//阻塞式IO
@Test
public void testClient1() throws IOException {
//1、获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
//2、获取文件通道
FileChannel fileChannel = FileChannel.open(Paths.get("爱情与友情.jpg"), StandardOpenOption.READ);
//3、创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//4、读取本地文件,发送到服务器
while (fileChannel.read(byteBuffer) != -1){
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
//5、关闭连接以进行写入,而不关闭通道。
socketChannel.shutdownOutput();
//6、接收服务器的反馈
int len = 0;
while ((len = socketChannel.read(byteBuffer)) != -1){
//7、读写切换
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,len));
//8、清除缓存
byteBuffer.clear();
}
//9、关闭通道
socketChannel.close();
fileChannel.close();;
}
@Test
public void testServer1() throws IOException {
//1、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2、打开文件通道
FileChannel fileChannel = FileChannel.open(Paths.get("爱情与友情9.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//3、将通道的套接字绑定到本地地址,并配置套接字以监听连接。
serverSocketChannel.bind(new InetSocketAddress(9999));
//4、接收客户端的连接
SocketChannel socketChannel = serverSocketChannel.accept();
//5、创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//6、读取数据并写入
while(socketChannel.read(byteBuffer) != -1){
byteBuffer.flip();
fileChannel.write(byteBuffer);
byteBuffer.clear();
}
//7、发送数据到客户端
byteBuffer.put("我是服务端,我已经成功接收到数据".getBytes());
byteBuffer.flip();
//8、将数据写入到通道
socketChannel.write(byteBuffer);
//9、关闭通道
socketChannel.close();
fileChannel.close();
serverSocketChannel.close();
}
- 非阻塞式IO
//非阻塞式IO
@Test
public void testclient2() throws IOException {
//1、获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
//2、切换非阻塞模式
socketChannel.configureBlocking(false);
//3、分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//4、发送数据给服务端
byteBuffer.put(("客户端传送时间数据:" + new Date()).getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
//5、关闭通道
socketChannel.close();
}
@Test
public void testServer2() throws IOException {
//1. 获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//3. 绑定连接
serverSocketChannel.bind(new InetSocketAddress(8888));
//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();
}
}
}
NIO2知识整理
- 新增Path接口,Paths工具类,Files工具类。 这些接口和工具类对NIO中的功能进行了高度封装,大大简化了文件系统的IO编程。
java.nio.file.Path
接口代表一个平台无关的平台路径,描述了目录结构中文件的位置。java.nio.file.Paths
仅由静态方法组成,通过转换路径字符串返回Path或URI 。java.nio.file.Files
用于操作文件或目录的工具类。
欢迎关注
公众号三筒记简介:分享各种编程知识、excel相关技巧、读书笔记