JAVANIO
总领
随着对高并发的需求,出现了nio,其核心就是缓冲区(Buffer)和通道(Channel),通道就是打开到IO设备(文件,套接字)
JavaNIO (New IO)从1.4引入的新IO API,是面向缓冲区的,基于通道的IO操作,更高效地进行文件读写(非阻塞式)。就是在文件与程序之间架设通道,由缓冲区负责数据的传输并且多了选择器(Selectors),网络编程
相关概念
同步与异步
- 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞和非阻塞
- 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在那里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(异步非阻塞)。
2.2 NIO的特性/NIO与IO区别
如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。
1)Non-blocking IO(非阻塞IO)
IO流是阻塞的,NIO流是不阻塞的。
Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
Java IO的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
2)Buffer(缓冲区)
IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。
3)Channel (通道)
NIO 通过Channel(通道) 进行读写。
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
4)Selectors(选择器)
NIO有选择器,而IO没有。
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
缓冲区存取
总概
缓冲区负责数据存取,底层是数组,用于不同类型数据。所以对于不同数据有不同缓冲区(ByteBuffer,CharBuffer等都是继承于Buffer类),不同缓冲区管理方式基本一致,
1通过allocate()获取缓冲区自定义大小
2缓冲区两个核心方法
put()存入数据到缓冲区(改变position大小)
溢出会抛出java.nio.BufferOverflowException异常
get()获取缓冲区数据(需要调用flip()切换至读模式,position=0,limit改变,随着get调用position会改变)
get()方法如果position=limit情况下使用会抛出java.nio.BufferUnderflowException异常
3缓冲区核心属性(本质数组)
capacity:最大存储容量,不能更改private
limit:表示缓冲区可以操作数据的大小,limit后数据不能进行读写
position:缓冲区当前操作数据的位置
mark用于mark()记录position位置
mark<=position<=limit<=capacity
如果var byteBuffer =ByteBuffer.allocate(1024);则limit=capacity
4flip()切换至读数据模式
public Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
返回值为当前实例
同时limit改变到已有数据之尾
5rewind()方法可重复读
返回值为当前实例
position变为0
6clear()清空
返回值为当前实例
但是缓冲区的数据还在,处于被遗忘状态,就是指针全部归位,仍存在。
7mark()标记
返回值为当前实例
标记当前position位置,通过reset()恢复到最近一次的mark位置.
8hasRemaining()
返回值位boolean,本质上就是比较position和limit大小
remaining(),返回值位int,本质上就是limit-position,可以操作的数量
直接缓冲区和非直接缓冲区
非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在jvm内存中
直接缓冲区:通过allocateDirect()方法分配缓冲区,将缓冲区建立在物理内存中
上面两种区别在于,非直接缓冲区需要一个copy的过程将内核地址空间的数据copy到用户地址空间,而直接缓冲区是用于物理内存的映射
直接缓冲区的分配与取消分配成本高于非直接缓冲区,直接缓冲区的内容可以驻留在常规垃圾回收堆外,所以容易受到基础系统的本机IO操作的大型的,持久的缓冲区用直接缓冲区,一般小就用非直接缓冲区
直接字节缓冲区还可以通过FileChannel的map()方法将文件区域直接映射到内存中来创建,该方法返回MappedByteBuffer,如果以上缓冲区的实例是不可访问区域,将会在某一时刻抛出不确定异常
字节缓冲区的类型判断可通过isDirect()方法确定,是为了能够在性能关键型代码中执行显式缓冲区管理
通道(Channel)的原理与获取
总概
Channel类似于传统的流,但是其本身不能访问数据,也不存储数据,只能与Buffer进行交互
主要实现类
java.nio.channels.Channel接口
FileChannel,SocketChannel,ServerSocketChannel
获取通道
1getChannel()方法
本地IO
FileInputStream/FileOutputStream
RandomAccessFile
网络IO
Socket
ServerSocket
DatagramSocket
2 jdk1.7NIO2
针对各个通道提供了静态方法open()
FileChannel.open
3jdk1.7NIO2
Files工具类的newByteChannel()
NIO通道数据传输和内存映射文件(直接比非直接快)
非直接缓冲区复制文件(读写)
try( FileInputStream fileInputStream = new FileInputStream("F:\\psbQ8LFKCE5.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("E:\\test.jpg");
FileChannel fileInputStreamChannel = fileInputStream.getChannel();
FileChannel fileOutputStreamChannel = fileOutputStream.getChannel();)
{ //缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
while (fileInputStreamChannel.read(byteBuffer)!=-1)
{
byteBuffer.flip();
fileOutputStreamChannel.write(byteBuffer);
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
直接缓冲区复制文件(读写)
程序和复制过程并不是同时结束,程序慢一点,会跑满内存和硬盘读写,和程序无关了只和系统有关,慢一点原因且随机的原因是gc
缓冲区的模式必须和通道模式一致,通道能读能写,缓冲区才能能读能写
try(FileChannel readChannel = FileChannel.open(Paths.get("F:\\psbQ8LFKCE5.jpg"), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Path.of("E:\\test.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW))
{
//get多个参数是将多个参数拼串,第二个参数是个枚举
//StandardOpenOption.CREATE_NEW存在就报错,不存在就报错,StandardOpenOption.CREATE存在就覆盖
//缓冲区在物理内存
//第一个参数为枚举类型
//map返回的就是一个直接缓存区
MappedByteBuffer readmappedByteBuffer = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
MappedByteBuffer writemappedByteBuffer = writeChannel.map(FileChannel.MapMode.READ_WRITE, 0, readChannel.size());
//直接对缓冲区进行数据读写
byte[] bytes = new byte[readmappedByteBuffer.limit()];
readmappedByteBuffer.get(bytes);
writemappedByteBuffer.put(bytes);
} catch (IOException e) {
e.printStackTrace();
}
通道之间的数据传输(直接缓冲区)
try( FileChannel readChannel = FileChannel.open(Paths.get("F:\\psbQ8LFKCE5.jpg"), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Path.of("E:\\test.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);) {
readChannel.transferTo(0, readChannel.size(), writeChannel);
//writeChannel.transferFrom(readChannel,0,readChannel.size());
} catch (IOException e) {
e.printStackTrace();
}
NIO分散读取和聚合写入
分散读取:将通道内的数据分散到多个缓冲区,依次填满缓冲区
聚合写入:将多个缓冲区数据聚集到通道中
由于汉字编码的奇特性,可能最后一个字会乱码,因为那个字编码不全(utf-8)
try {
RandomAccessFile randomAccessFile = new RandomAccessFile("F:\\test.txt", "rw");
FileChannel randomAccessFileChannel = randomAccessFile.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
ByteBuffer byteBuffer1 = ByteBuffer.allocate(100);
ByteBuffer[] byteBuffers={byteBuffer,byteBuffer1};
//分散读取,参数是一个缓冲区数组
randomAccessFileChannel.read(byteBuffers);
Arrays.stream(byteBuffers).map(x->x.flip())
.forEach((ByteBuffer b)-> System.out.println(new String(b.array(),0,b.limit())));
//聚合写入
new RandomAccessFile("F:\\2.txt", "rw")
.getChannel()
.write(byteBuffers);
randomAccessFile.close();
} catch (IOException e) {
e.printStackTrace();
}
字符集Charset
会有IOException
Charset cs1 = Charset.forName("GBK");
//获取编码器
CharsetEncoder ce = cs1.newEncoder();
//获取解码器
CharsetDecoder cd = cs1.newDecoder();
CharBuffer cBuf = CharBuffer.allocate(1024);
cBuf.put("test");
cBuf.flip();
//编码
ByteBuffer bBuf = ce.encode(cBuf);
for (int i = 0; i < 12; i++) {
System.out.println(bBuf.get());
}
//解码
bBuf.flip();
CharBuffer cBuf2 = cd.decode(bBuf);
System.out.println(cBuf2.toString());
Charset cs2 = Charset.forName("GBK");
bBuf.flip();
CharBuffer cBuf3 = cs2.decode(bBuf);
System.out.println(cBuf3.toString());