NIO是New I/O的简称,与旧式的基于流的I/O方法相对,从名字看,它表示新的一套Java I/O标准。
具有以下特性:
传统Java IO,它是阻塞的,低效的。那么Java NIO和传统Java IO有什么不同?带来了什么?
(1)面向块的I/O
传统JavaIO是面向流的I/O。流I/O一次处理一个字节。NIO则是面向块的I/O,每次操作都是以数据块为单位。它们的差距就好象两个人吃饭,一个人一粒一粒的吃,另一个人狼吞虎咽,快慢显而易见。
NIO中引入了缓冲区(Buffer)的概念,缓冲区作为传输数据的基本单位块,所有对数据的操作都是基于将数据移进/移出缓冲区而来;读数据的时候从缓冲区中取,写的时候将数据填入缓冲区。尽管传统JavaIO中也有相应的缓冲区过滤器流(BufferedInputStream等),但是移进/移出的操作是由程序员来包装的,它本质是对数据结构化和积累达到处理时的方便,并不是一种提高I/O效率的措施。NIO的缓冲区则不然,对缓冲区的移进/移出操作是由底层操作系统来实现的。
通常一次缓冲区操作是这样的:某个进程需要进行I/O操作,它执行了一次读(read)或者写(write)的系统调用,向底层操作系统发出了请求,操作系统会按要求把数据缓冲区填满或者排干。说起来简单,其实很复杂。但至少我们知道了这事是由操作系统干的,比我们代码级的实现要高效的多。
除了效率上的差别外,缓冲区在数据分析和处理上也带来的很大的便利和灵活性。
(2)非阻塞的I/O + 就绪性选择
传统JavaIO是基于阻塞I/O模型的:当发起一个I/O请求时,如果数据没有准备好(read时无可读数据,write时数据不可写入),那么线程便会阻塞,直到数据准备好,导致线程大部分的时间都在阻塞。
而非阻塞I/O则允许线程在有数据的时候处理数据,没有数据的时候干点别的,提高了资源利用率。
就绪性选择通常是建立在非阻塞的基础上,并且更进一步,它把检查哪些I/O请求的数据准备好这个任务交给了底层操作系统,操作系统会去查看并返回结果集合,这样我们只需要关心那些准备好进行操作的IO通道。关于就绪性选择的过程会在后面详述。
NIO提供的Socket可以用非阻塞的方式工作,并且支持就绪性选择,减少了资源消耗和CPU在线程间的切换,在管理线程效率上比传统Socket高。
(3)文件锁定和内存映射文件等操作系统特性
NIO同时带来了很多当今操作系统大都支持的特性。
文件锁定是多个进程协同工作的情况下,要协调进程间对共享数据的访问必不可少的工具。
内存映射利用虚拟内存技术提供对文件的高速缓存,使读取磁盘文件就像从内存中读取一样高效,但是却不会有内存泄漏的危险,因为在内存中不会存在文件的完整拷贝。
此外还有一些其他的特性,后面再详述。
(4)为所有的原始类型提供(Buffer)缓存支持
(5)使用Java.nio.charset.Charset作为字符集编码解码解决方案
(6)增加通道(Channel)对象,作为新的原始I/O抽象
(7)提供了基于Selector的异步网络I/O。
为什么要使用NIO?
对于文件I/O, 在我看来使用IO和NIO是区别不大的,Java1.4开始原始IO也根据NIO重新实现过了,提供了对于NIO特性的支持。即使是流,也会比以前更加高效。企业级应用软件中涉及I/O的部分多半是读写文件的功能性需求,很少有在并发上的要求,那么JavaIO包已经很胜任了。
对于网络I/O,传统的阻塞式I/O,一个线程对应一个连接,采用线程池的模式在大部分场景下简单高效。当连接数茫茫多时,并且数据的移动非常频繁,NIO无疑是更好的选择。
NIO标榜的是高速、可伸缩的I/O,因为它更亲近操作系统。当需求很平凡,没有太高的效率要求的时候,你看不出它的好,反而觉得NIO代码实现复杂,不易理解。选择与否全看使用的场景,这点就看使用者的权衡了。
NIO的Buffer类族和Channel
在NIO中和Buffer配合使用的还有Channel。Channel是一个双向通道,即可读,也可写。有点儿类似Stream,但是Stream是单向的。应用程序中不能直接对Channel进行读写操作,而必须通过Buffer来进行。比如,在读一个Channel的时候,需要先将数据读入到相对应的Buffer,然后在Buffer中进行读取。
文件复制示例如下:
public static void nioCopyFile(String resource, String destination){
try {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel();
FileChannel writeChannel = fos.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len;
while((len=readChannel.read(buffer))!=-1){
buffer.flip();
writeChannel.write(buffer);
buffer.clear();
}
readChannel.close();
writeChannel.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
文件映射到内存
NIO提供了一种将文件映射到内存的方法进行I/O操作,它可以比常规的基于流的I/O快很多。这个操作主要由FileChannel.map()方法实现,比如:
MappredByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
以上代码将文件的前1024个字节映射到内存中。map()方法返回一个MappredByteBuffer,它是ByteBuffer的子类。因此,可以像使用ByteBuffer那样使用它。
public static void nioCopyFile(String resource, String destination){
try { RandomAccessFile fis = new RandomAccessFile(resource, "rw");
RandomAccessFile fos = new RandomAccessFile(destination, "rw");
FileChannel readChannel = fis.getChannel();
FileChannel writeChannel = fos.getChannel();
MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_WRITE, 0, readChannel.size());
writeChannel.write(mbb);
readChannel.close();
writeChannel.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
或者如下写法:
public static void nioCopyFile(String resource, String destination){
try {
File res = new File(resource);
File dest = new File(destination);
if(!dest.exists())
dest.createNewFile();
FileInputStream fis = new FileInputStream(res);
FileOutputStream fos = new FileOutputStream(dest);
FileChannel readChannel = fis.getChannel();
FileChannel writeChannel = fos.getChannel();
MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
writeChannel.write(mbb);
readChannel.close();
writeChannel.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
或者如下写法:
public static void nioCopyFile(String resource, String destination){
try {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
FileChannel readChannel = fis.getChannel();
FileChannel writeChannel = fos.getChannel();
MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
writeChannel.write(mbb);
readChannel.close();
writeChannel.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
处理结构化数据
NIO还提供了处理结构化数据的方法,称之为散射(Scattering)和聚集(Gathering)。散射指将数据读入一组Buffer中,而不仅仅是一个。聚集与之相反,指将数据写入一组Buffer中。
假设有文本文件,格式为“书名作者”,现通过聚集写操作创建该文件和散射读文件:
public static void readAndWrite(){
try {
//聚集写操作
ByteBuffer bookBuf = ByteBuffer.wrap("java性能优化技巧".getBytes("utf-8"));
ByteBuffer autBuf = ByteBuffer.wrap("葛一鸣".getBytes("utf-8"));
int booklen = bookBuf.limit(); //记录书名长度
int authlen = autBuf.limit(); //记录作者长度
ByteBuffer[] bufs = new ByteBuffer[]{bookBuf, autBuf};
File file = new File("D:\\book.txt");
if(!file.exists())
file.createNewFile(); //文件不存在则创建文件
FileOutputStream fos = new FileOutputStream(file);
FileChannel fc = fos.getChannel();
fc.write(bufs);
fos.close();
fc.close();
//散射读操作
ByteBuffer b1 = ByteBuffer.allocate(booklen);
ByteBuffer b2 = ByteBuffer.allocate(authlen);
ByteBuffer[] buffs = new ByteBuffer[]{b1,b2};
FileInputStream fin = new FileInputStream(file);
FileChannel fic = fin.getChannel();
fic.read(buffs);
fin.close();
fic.close();
String bookName = new String(b1.array(), "utf-8");
String authName = new String(b2.array(), "utf-8");
System.out.println(bookName+" "+authName);;
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
直接内存访问
NIO的Buffer还提供了一个可以直接访问系统物理内存的类——DirectBuffer。DirectBuffer继承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍然在JVM堆上分配空间,其最大内存,受到最大堆的限制。而DirectBuffer直接分配在物理内存中,并不占用堆空间。使用DirectBuffer是一个更接近系统底层的方法,所以,它的速度比普通的ByteBuffer更快。
申请DirectBuffer的方法如下:
ByteBuffer b = ByteBuffer.allocateDirect(500);
虽然访问速度上有优势,但是创建和销毁DirectBuffer的花费却远比ByteBuffer高。因此在需要频繁创建Buffer的场合,不宜使用DirectBuffer,但是如果能将DirectBuffer进行复用,那么,在读写频繁的情况下,它完全可以大幅改善系统性能。
将DirectBuffer应用于真实系统中,不可避免地还需要对DirectBuffer进行监控。下面是一段可用于DirectBuffer监控的代码,增强DirectBuffer的可用性:
//这段代码用于监控DirectBuffer的使用情况
public void monDirectBuffer() throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{
Class c = Class.forName("java.nio.Bits"); //通过反射取得私有数据
Field maxMemory = c.getDeclaredField("maxMemory");
maxMemory.setAccessible(true);
Field reservedMemory = c.getDeclaredField("reservedMemory");
reservedMemory.setAccessible(true);
synchronized(c){
Long maxMemoryValue = (Long) maxMemory.get(null); //总大小
Long reservedMemoryValue = (Long) reservedMemory.get(null); //剩余大小
System.out.println("maxMemoryValue:"+maxMemoryValue);
System.out.println("reservedMemoryValue:"+reservedMemoryValue);
}
}