Java的NIO
BufferedReader有一个特征,就是读取输入流中的数据时,如果没有读到有效数据,程序将在此处阻塞该线程的执行(使用InputStream的read方法从流中读取数据时,也有这样的特性),java.io下面的输入输出流都是阻塞式的。不仅如此,传统的输入输出流都是通过字节的移动来处理的,及时我们不直接去处理字节流,单底层的实现还是依赖于字节处理,也就是说,面向流的输入输出系统一次只能处理一个字节,因此面向流的输入输出系统通常效率都不高。
为了解决上面的问题,NIO就出现了。NIO采用内存映射文件来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了。(这种方式模拟了操作系统上虚拟内存的概念),通过这种方式进行输入输出要快得多。
Channel(通道)和Buffer(缓冲)是NIO中两个核心对象。Channel是对传统的输入输出系统的模拟,在NIO系统中所有的数据都需要通过通道传输,Channel与传统的InputStream、OutputStream最大的区别就是提供了一个map()方法,通过它可以直接将“一块数据”映射到内存中。如果说传统IO是面向流的处理,那么NIO是面向块的处理。
Buffer可以被理解为一个容器,它的本质是一个数组,发送到Channel中所有的对象都必须首先放到Buffer中,从而Channel中读取的数据也必须先放到Buffer中。Buffer既可以一次次从Channel取数据,也可以使用Channel直接将文件的某块数据映射成Buffer。
除了Channel和Buffer之外,NIO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类,也提供了用于支持非阻塞式输入输出的Selector类。
Buffer介绍
在Buffer中有三个重要的概念:
- 容量(capacity):表示Buffer的大小,创建后不能呢过改变;
- 界限(limit):第一个不能被读写的缓冲区的位置,也就是后面的数据不能被读写;
- 位置(position):用于指明下一个可以被读写的缓冲区位置索引。当从Channel读取数据的时候,position的值就等于读到了多少数据。
Buffer的主要作用就是装入数据,然后输出数据。当装入数据结束时,调用flip()方法,该方法将limit设为position所在位置,将position的值设为0,为输出数据做好准备。当输出数据结束后,调用clear()方法,将position的值设为0,limit设为capacity,为下一次的装入数据做准备。
常用的Buffer是CharBuffer和ByteBuffer。
使用put()和get()方法进行数据的放入和读取,分为相对和绝对两种:
- 相对:从Buffer当前position位置开始读取或者写入数据,然后将position的值按处理元素的个数增加;
- 绝对:直接根据索引向Buffer中读取和写入数据,使用绝对方式访问Buffer里的数据时,不会影响position的值。
代码示例
package com.wangjun.othersOfJava;
import java.nio.CharBuffer;
public class NIOBufferTest {
public static void main(String[] args) {
//创建CharBuffer
CharBuffer cb = CharBuffer.allocate(8);
System.out.println("capacity:" + cb.capacity());
System.out.println("limit:" + cb.limit());
System.out.println("position:" + cb.position());
//放入元素
cb.put('a');
cb.put('b');
cb.put('c');
System.out.println("加入三个元素后,position:" + cb.position());
cb.flip();
System.out.println("执行flip()后,limit:" + cb.limit());
System.out.println("执行flip()后,position:" + cb.position());
System.out.println("取出第一个元素: " + cb.get());
System.out.println("取出第一个元素后,position:" + cb.position());
//调用clear方法
cb.clear();
System.out.println("执行clear()后,limit:" + cb.limit());
System.out.println("执行clear()后,position:" + cb.position());
System.out.println("执行clear()后,数据没有清空,第三个值是:" + cb.get(2));
System.out.println("执行绝对读取后,position:" + cb.position());
}
}
通过allocate方法创建的是普通Buffer,还可以通过allocateDirect方法来创建直接Buffer,虽然创建成本比较高,但是读写快。因此适用于长期生存的Buffer,使用方法和普通Buffer类似。注意,只有ByteBuffer提供了此方法,其他类型的想用,可以将该Buffer转成其他类型的Buffer。
Channel(通道)介绍
Channel类似传统的流对象,主要区别如下:
- Channel可以直接将指定文件的部分或全部直接映射成Buffer。
- 程序不能直接访问Channel中的数据,只能通过Buffer交互。
所有的Channel都不应该通过构造器来创建,而是通过传统的InputStream、OutputStream的getChannel()方法来返回对应的Channel,不同的节点流获取的Channel不一样,比如FileInputStream返回的是FileChannel。
Channel常用的方法有三类:map()、read()、write()。map方法将Channel对应的部分或全部数据映射成ByteBuffer;read和write方法都有一系列的重载形式,这些方法用于从Buffer中读取/写入数据。
代码示例
package com.wangjun.othersOfJava;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
/*
* 将FileChannel的全部数据映射成ByteBuffer
*/
public class NIOChannelTest {
public static void main(String[] args) throws Exception {
File f = new File("NIOChannelTest.md");
//java7新特性try括号内的资源会在try语句结束后自动释放,前提是这些可关闭的资源必须实现 java.lang.AutoCloseable 接口。
try(
FileChannel inChannel = new FileInputStream(f).getChannel();
FileChannel outChannel = new FileOutputStream("a.md").getChannel();
){
//将FileChannel的全部数据映射成ByteBuffer
//map方法的三个参数:1映射模式;2,3控制将哪些数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
//直接将buffer的数据全部输出,完成文件的复制: NIOChannelTest.md -> a.md
outChannel.write(buffer);
//以下为了输出文件字符串内容
//使用GBK字符集来创建解码器
Charset charset = Charset.forName("GBK");
//复原limit和position的位置
buffer.clear();
//创建解码器
CharsetDecoder decoder = charset.newDecoder();
//使用解码器将ByteBuffer转成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
System.out.println(charBuffer);
}
}
}
除了上面的一次性取数据,也可以分多次取数据
//如果Channel对应的文件过大,使用map方法一次性将所有文件内容映射到内存中可能会因此性能下降
//这时候我们可以适应Channel和Buffer进行多次重复取值
public static void getDataByArray() throws Exception {
try(
//创建文件输入流
FileInputStream fileInputStream = new FileInputStream("NIOChannelTest.md");
FileChannel fileChannel = fileInputStream.getChannel();
){
//定义一个ByteBuffer对象,用于重复取水
ByteBuffer bbuff = ByteBuffer.allocate(64);
while(fileChannel.read(bbuff) != -1) {
bbuff.flip();
Charset charset = Charset.forName("GBK");
//创建解码器
CharsetDecoder decoder = charset.newDecoder();
//使用解码器将ByteBuffer转成CharBuffer
CharBuffer charBuffer = decoder.decode(bbuff);
System.out.println(charBuffer);
//为下一次读取数据做准备
bbuff.clear();
}
}
}
文件锁
在NIO中,Java提供了文件锁的支持,使用FileLock来支持文件锁定功能,在FileChannel中提供lock()/tryLock()方法来获取文件锁FileLock对象,从而锁定文件。lock和tryLock的区别是前者无法得到文件锁的时候会阻塞,后者不会阻塞。也支持锁定部分内容,使用lock(long position, long size, boolean shared)即可,其中shared为true时,表明该锁是一个共享锁,可以允许多个县城读取文件,但阻止其他进程获得该文件的排他锁。当shared为false时,表明是一个排他锁,它将锁住对该文件的读写。
默认获取的是排他锁。
代码示例
package com.wangjun.othersOfJava;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
public class FileLockTest {
public static void main(String[] args) throws Exception {
try(
FileOutputStream fos = new FileOutputStream("a.md");
FileChannel fc = fos.getChannel();
){
//使用非阻塞方式对指定文件加锁
FileLock lock = fc.tryLock();
Thread.sleep(3000);
lock.release();//释放锁
}
}
}
Java的NIO2
Java7对原来的NIO进行了重大改进:
- 提供了全面的文件IO和文件系统访问支持;
- 基于异步Channel的IO。
这里先简单介绍一下对文件系统的支持,后续继续学习。
NIO2提供了一下接口和工具类:
- Path接口:通过和Paths工具类结合使用产生Path对象,可以获取文件根路径、绝对路径、路径数量等;
- Files工具类:提供了很多静态方法,比如复制文件、一次性读取文件所有行、判断是否为隐藏文件、判断文件大小、遍历文件和子目录、访问文件属性等;
- FileVisitor接口:代表一个文件访问器,Files工具类使用walkFileTree方法遍历文件和子目录时,都会触发FileVisitor中相应的方法,比如访问目录之前、之后,访问文件时,访问文件失败时;
- WatchService:监控文件的变化;
NIO和IO的区别
Java的NIO提供了与标准IO不同的工作方式:
- Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,没有被缓存在任何地方,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,因此可以前后移动流中的数据;
- Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。因为NIO将阻塞交给了后台线程执行。而IO是阻塞的;
- Selectors(选择器):选择器允许一个单独的线程可以监听多个数据通道((网络连接或文件),你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
使用场景
NIO
- 优势在于一个线程管理多个通道;但是数据的处理将会变得复杂;
- 如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,采用这种;
传统的IO
- 适用于一个线程管理一个通道的情况;因为其中的流数据的读取是阻塞的;
- 如果需要管理同时打开不太多的连接,这些连接会发送大量的数据;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。