什么是NIO
1、Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始), Java NIO提供了与标准IO不同的IO工作方式。
2、Java NIO: Channels and Buffers(通道和缓冲区)
3、标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
4、Java NIO: Non-blocking IO(非阻塞IO)
5、Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
6、Java NIO: Selectors(选择器)
7、Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
Buffer的概述
Buffer就像一个数组,可以保存多个相同类型的数据。根据类型不同(boolean除外),有以下Buffer常用子类:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffe
- 容量(capacity): 表示Buffer最大数据容量,缓冲区容量不能为负,并且建立后不能修改;
- 限制(limit): 第一个不应该读取或者写入的数据的索引,即位于limit后的数据不可以读写。缓冲区的限制不能为负,并且不能大于其容量(capacity);
- 位置(position): 下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制(limit);
- 标记(mark)与重置(reset): 标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position;
- 不变量 :
标记,位置,极限和容量值的以下不变量保持不变:
0 <= mark <= position <= limit <= capacity
新创建的缓冲区始终具有零位置和未定义的标记。 初始限制可以为零,或者可以是取决于缓冲器的类型和构造方式的某些其他值。 新分配的缓冲区的每个元素被初始化为零。
操作Buffer示例
@Test
public void test() {
// 1.指定缓冲区大小1024
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("--------------------");
System.out.println(buf.position());//输出0
System.out.println(buf.limit());//输出1024
System.out.println(buf.capacity());//输出1024
// 2.向缓冲区存放5个字节
buf.put("abcd1".getBytes());
System.out.println("--------------------");
System.out.println(buf.position());//输出5
System.out.println(buf.limit());//输出1024
System.out.println(buf.capacity());//输出1024
// 3.开启读模式
/**
* flip()使缓冲区准备好新的通道写入或相对获取操作序列:它将限制设置为当前位置,
* 然后将位置设置为零。如果标记被定义,则它被丢弃。在通道读取或放置操作的序列后,
* 调用此方法来准备一系列通道写入或相对获取操作
*/
buf.flip();
System.out.println("----------开启读模式...----------");
System.out.println(buf.position());//输出0
System.out.println(buf.limit());//输出5
System.out.println(buf.capacity());//输出1024
byte[] bytes = new byte[buf.limit()];
buf.get(bytes);//此方法将字节从此缓冲区(buf)传输到给定的目标数组(bytes[])
System.out.println(new String(bytes, 0, bytes.length));//输出“abcd1”
System.out.println("----------重复读模式...----------");
// 4.开启重复读模式
/**
* rewind() 倒带这个缓冲区。 位置设置为零,标记被丢弃。
* 使缓冲区准备好重新读取已经包含的数据:它保持限制不变,并将位置设置为零。
*/
buf.rewind();
System.out.println(buf.position());//输出0
System.out.println(buf.limit());//输出5
System.out.println(buf.capacity());//输出1024
byte[] bytes2 = new byte[buf.limit()];
buf.get(bytes2);
System.out.println(new String(bytes2, 0, bytes2.length));//输出“abcd1”
System.out.println("----------清空缓冲区...----------");
// 5.clean 清空缓冲区 数据依然存在,只不过数据被遗忘
//位置设置为零,限制设置为容量,标记被丢弃。
//在使用一系列通道读取或放置操作填充此缓冲区之前调用此方法
buf.clear();
System.out.println(buf.position());//0
System.out.println(buf.limit());//1024
System.out.println(buf.capacity());//1024
System.out.println((char)buf.get());//输出a(说明数据未清空,只是被遗忘)
}
make与reset用法
标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
@Test
public void testMark_Rest() {
ByteBuffer buf = ByteBuffer.allocate(1024);
String str = "abcd1";
buf.put(str.getBytes());
// 开启读取模式
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst, 0, 2);
buf.mark();//在position==2位置做一个标记
System.out.println(new String(dst, 0, 2));//ab
System.out.println(buf.position());//2
buf.get(dst, 2, 2);//position==4
System.out.println(new String(dst, 2, 2));//cd
System.out.println(buf.position());//4
buf.reset();//postion==2
System.out.println("重置恢复到mark位置..");
buf.get(dst, 3, 2);//position==4
System.out.println(new String(dst, 2, 2));//cc
System.out.println(buf.position());//4
}
直接缓冲区与非直接缓冲区别
非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中,相对于直接缓冲区要安全些。
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 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 进行交互。
java.nio.channels.Channel 接口:
|--FileChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
Java 针对支持通道的类提供了 getChannel() 方法
本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile
网络IO:
Socket
ServerSocket
DatagramSocket
在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
通道与直接缓冲区使用示例
@Test
// 使用直接缓冲区完成文件的复制(內存映射文件) //428、357
public void test2() throws IOException {
long startTime = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("g://1.mp4"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("g://2.mp4"),StandardOpenOption.READ,
StandardOpenOption.WRITE,StandardOpenOption.CREATE;
// 映射文件
MappedByteBuffer inMapperBuff = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapperBuff = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
// 直接对缓冲区进行数据读写操作
byte[] dst = new byte[inMapperBuff.limit()];
inMapperBuff.get(dst);
outMapperBuff.put(dst);
outChannel.close();
inChannel.close();
long endTime = System.currentTimeMillis();
System.out.println("内存映射文件耗时:"+(endTime-startTime));
}
@Test
// 1.利用通道完成文件复制(非直接缓冲区)
public void test1() throws IOException { //11953 、3207、3337
long startTime = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("g://1.mp4");
FileOutputStream fos = new FileOutputStream("g://2.mp4");
// ①获取到通道
FileChannel inChannel = fis.getChannel();
FileChannel 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();
long endTime = System.currentTimeMillis();
System.out.println("非缓冲区:"+(endTime-startTime));
}