本篇博文主要包含:
- NIO的基本概念
- NIO中Buffer缓冲区数据存取
- 直接缓冲区与非直接缓冲区别
- 分散读取与聚集写入
- 字符集 Charset
-编码:字符串->字节数组
-解码:字节数组 -> 字符串
一、NIO概述
Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。
Java NIO: Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Java NIO: Non-blocking IO(非阻塞IO)
Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
Java NIO: Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
注意: 传统IO是单向; NIO是双向的。
IO与NIO的区别:
IO | NIO |
---|---|
面向流 | 面向缓冲区 |
阻塞IO | 非阻塞IO |
无 | 选择器 |
二、Buffer的数据存取
一个用于特定基本数据类型的容器。有java.nio包定义的,所有缓冲区都是抽象类Buffer的子类。
Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入到缓冲区,从缓冲区写入通道中的。
Buffer就像一个数组,可以保存多个相同类型的数据。根据类型不同(boolean除外),有以下Buffer常用子类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- buffer的常用属性:
1)容量(capacity):表示Buffer最大数据容量,缓冲区容量不能为负,并且建立后不能修改。
2)限制(limit):第一个不应该读取或者写入的数据的索引,即位于limit后的数据不可以读写。缓冲区的限制不能为负,并且不能大于其容量(capacity)。
3)位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制(limit)。
4)标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
代码演示:
@Test
public void bufferDemo1(){
try {
ByteBuffer allocate = ByteBuffer.allocate(1024);
System.out.println("缓冲区最大容量: "+allocate.capacity());
System.out.println("缓冲区可以操作的数据大小: "+allocate.limit());
System.out.println("缓冲区下一个操作的位置: "+allocate.position()); //下标从0开始
System.out.println("----------------向缓冲区存放数据----------------------------");
allocate.put("12345".getBytes());
System.out.println("缓冲区最大容量: "+allocate.capacity());
System.out.println("缓冲区可以操作的数据大小: "+allocate.limit());
System.out.println("缓冲区下一个操作的位置: "+allocate.position());
System.out.println("----------------开启读模式----------------------------");
allocate.flip(); //从下标0开始读取
System.out.println("缓冲区最大容量: "+allocate.capacity());
System.out.println("缓冲区可以操作的数据大小: "+allocate.limit()); //此时输出为5
System.out.println("缓冲区下一个操作的位置: "+allocate.position()); //此时输出为0
byte[] bytes = new byte[allocate.limit()];
allocate.get(bytes);
System.out.println(new String(bytes, 0, bytes.length));
System.out.println("----------------重复读模式----------------------------");
allocate.rewind(); //从上次读取的地方开始读取
System.out.println("缓冲区最大容量: "+allocate.capacity());
System.out.println("缓冲区可以操作的数据大小: "+allocate.limit()); //此时输出为5
System.out.println("缓冲区下一个操作的位置: "+allocate.position());
byte[] bytes2 = new byte[allocate.limit()];
allocate.get(bytes2);
System.out.println(new String(bytes2, 0, bytes2.length));
// clean 清空缓冲区 数据依然存在,只不过数据被遗忘
System.out.println("----------清空缓冲区...----------");
allocate.clear();
System.out.println("缓冲区最大容量: "+allocate.capacity());
System.out.println("缓冲区可以操作的数据大小: "+allocate.limit());
System.out.println("缓冲区下一个操作的位置: "+allocate.position());
byte[] bytes3 = new byte[allocate.limit()];
allocate.get(bytes3);
System.out.println(new String(bytes3, 0, bytes3.length));
} catch (Exception e) {
e.printStackTrace();
}
}
运行结果:
- make与rest用法
标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
代码演示:
@Test
public void bufferDemo2() {
ByteBuffer allocate = ByteBuffer.allocate(1024);
allocate.put("abcd".getBytes());
allocate.flip();
byte[] bytes = new byte[allocate.limit()];
allocate.get(bytes,0,2); //开启读模式
System.out.println("输出数据:"+new String(bytes,0,2));
System.out.println("缓冲区最大容量: "+allocate.capacity());
System.out.println("缓冲区可以操作的数据大小: "+allocate.limit());
System.out.println("缓冲区下一个操作的位置: "+allocate.position());
System.out.println("----------打印标记----------");
allocate.mark(); //打印标记
allocate.get(bytes,2,2);
System.out.println("输出数据:"+new String(bytes,2,2));
System.out.println("缓冲区最大容量: "+allocate.capacity());
System.out.println("缓冲区可以操作的数据大小: "+allocate.limit());
System.out.println("缓冲区下一个操作的位置: "+allocate.position());
System.out.println("----------还原到mark位置----------");
allocate.reset(); //还原到mark位置
System.out.println("缓冲区最大容量: "+allocate.capacity());
System.out.println("缓冲区可以操作的数据大小: "+allocate.limit());
System.out.println("缓冲区下一个操作的位置: "+allocate.position());
}
运行结果:
三、直接缓冲区与非直接缓冲区别
-
非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中。
-
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。 -
通道(Channel)的原理
通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。Channel 负责传输, Buffer 负责存储。通道是由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。
直接缓冲区代码演示:
//直接缓冲区
@Test
public void directBuffer() throws IOException {
long starTime = System.currentTimeMillis();
//创建管道
FileChannel inChannel = FileChannel.open(Paths.get("fc1.png"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("fc2.png"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//定义映射文件
MappedByteBuffer inMapBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区操作
byte[] dsf = new byte[inMapBuffer.limit()];
inMapBuffer.get(dsf);
outMapBuffer.put(dsf);
inChannel.close();
outChannel.close();
long endTime = System.currentTimeMillis();
System.out.println("操作直接缓冲区耗时时间:"+(endTime-starTime));
}
运行结果:
非直接缓冲区代码演示:
//非直接缓冲区
@Test
public void notDirectBuffer() throws IOException {
long starTime = System.currentTimeMillis();
//读入流
FileInputStream fis = new FileInputStream("fc1.png");
//写入流
FileOutputStream fos = new FileOutputStream("fc3.png");
//创建通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
//分配并指定缓冲区大小
ByteBuffer allocate = ByteBuffer.allocate(1024);
while(inChannel.read(allocate) !=-1) {
//开启读取模式
allocate.flip();
//将数据写入到通道中
outChannel.write(allocate);
allocate.clear();
}
//关闭通道、关闭连接
fis.close();
fos.close();
inChannel.close();
outChannel.close();
long endTime = System.currentTimeMillis();
System.out.println("操作非直接缓冲区耗时时间:"+(endTime-starTime));
}
运行结果:
四、分散读取与聚集写入
- 分散读取(scattering Reads):将通道中的数据分散到多个缓冲区中。
- 聚集写入(gathering Writes):将多个缓冲区的数据聚集到通道中。
代码演示:
//分散读取聚集写入
@Test
public void ScatteredReadsClusteredWrites() throws IOException {
//随机访问
RandomAccessFile raf = new RandomAccessFile("fc1.png", "rw");
//获取通道
FileChannel channel = raf.getChannel();
//分配指定大小指定缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//分散读取
ByteBuffer[] bufs = {buf1,buf2};
channel.read(bufs);
for(ByteBuffer bb:bufs) {
//切换成读模式
bb.flip();
}
//聚集写入
RandomAccessFile raf2 = new RandomAccessFile("fc4.png", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
raf.close();
raf2.close();
}
五、字符集 Charset
编码:字符串->字节数组
解码:字节数组 -> 字符串
代码演示:
//字符集 Charset
@Test
public void charset() throws CharacterCodingException {
// 获取编码器
Charset cs1 = Charset.forName("GBK");
// 获取加密器
CharsetEncoder ce = cs1.newEncoder();
// 获取解码器
CharsetDecoder cd = cs1.newDecoder();
CharBuffer cBuf = CharBuffer.allocate(1024);
cBuf.put("不抛弃不放弃");
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());
System.out.println("-------------------------------------");
Charset cs2 = Charset.forName("utf-8");
bBuf.flip();
CharBuffer cbeef = cs2.decode(bBuf);
System.out.println(cbeef.toString());
}
运行结果 :