NIO特性
1、为原始类提供缓存支持;
2、字符集编码解码解决方案;
3、Channel :一个新的原始 I/O 抽象;
4、支持锁和内存映射文件的文件访问接口;
5、提供多路非阻塞式(non-bloking)的高伸缩性网络 I/O。
Buffer
在基本I/O操作中所有的操作都是直接以流的形式完成的,而在NIO中所有的操作都要使用到缓冲区处理,且所有的读写操作都是通过缓冲区完成的。缓冲区(Buffer)是一个线性的、有序的数据集,只能容纳某种特定的数据类型。
一个Buffer有以下几个属性:
容量(capacity):缓冲区能包含的元素的最大数目。
限制(limit):第一个无法被写入或读取的元素坐标。
坐标(position):下一个要写入或读取的元素的坐标。
更多Buffer的属性和方法参考http://download.oracle.com/javase/6/docs/api/ 。
另外,
http://www.linuxtopia.org/online_books/programming_books/thinking_in_java/TIJ314_027.htm 上面有一个各种类型的Buffer和byte[]之间相互转换的模型图,挺有帮助的。
图 1 Buffer内部结构
看一下一个简单的示例,演示缓冲区的操作流程,观察position、limit、capacity。
Java代码:
import java.nio.IntBuffer;
public class IntBufferDemo {
public static void main(String[] args) {
// 开辟10个大小的缓冲区
IntBuffer buf = IntBuffer.allocate(10);
System.out.print("1、写入数据之前的position、limit、capacity:");
System.out.println("position= " + buf.position() + ",limit="
+ buf.limit() + ",capacity=" + buf.position());
// 定义整形数组
int temp[] = { 5, 3, 2 };
// 向缓冲区写入数据
buf.put(3);
// 向缓冲区写入一组数据
buf.put(temp);
System.out.print("2、写入数据之后的position、limit、capacity:");
System.out.println("position= " + buf.position() + ",limit="
+ buf.limit() + ",capacity=" + buf.position());
// 重设缓冲区
buf.flip();
System.out.print("3、重设缓冲区后的position、limit、capacity:");
System.out.println("position= " + buf.position() + ",limit="
+ buf.limit() + ",capacity=" + buf.position());
System.out.print("缓冲区中的内容");
// 只要缓冲区还有内容则输出
while (buf.hasRemaining()) {
System.out.print(buf.get() + " ");
}
}
}
直接缓冲区
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
使用allocateDirect方法完成这个操作。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。
字符编码解码
Java中的信息是用Unicode进行编码的,但是现实世界的编码有很多种,这就有了编码、解码的问题。Java使用Charset来表示一个字符集,希望用一种统一的方法来解决编码解码问题。下面看一个实际的例子。
Java代码:
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
public class CharsetDemo {
public static void main(String[] args) throws Exception {
Charset latin1 = Charset.forName("GBK");
CharsetEncoder encoder = latin1.newEncoder();
CharsetDecoder decoder = latin1.newDecoder();
CharBuffer cb = CharBuffer.wrap("我是实习生。");
ByteBuffer buf = null;
buf = encoder.encode(cb);
System.out.println(decoder.decode(buf));
}
}
通道
使用通道(Channel)来读取和写入数据,所有的内容都是先读到或写入到缓冲区中,再通过缓冲区操作的。通道与传统的流操作不同,传统的流操作分为输入和输出,而通道支持双向操作。java.nio.channels.Channel是一个接口,只定义了close()和isOpen()方法。
通道中比较常用的就是FileChannel了,注意,FileChannel是一个抽象类,所以通过别的对象产生。下面给出一个FileChannel的示例。
Java代码:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo {
public static void main(String[] args) throws Exception {
File file1 = new File("in.txt");
File file2 = new File("out.txt");
FileInputStream input = new FileInputStream(file1);
FileOutputStream output = new FileOutputStream(file2);
FileChannel fin = input.getChannel();
FileChannel fout = output.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
while (fin.read(buf) != -1) {
buf.flip();
fout.write(buf);
buf.clear();
}
fin.close();
fout.close();
input.close();
output.close();
}
}
内存映射
内存映射可以把文件映射到内存(MappedByteBuffer)中,这样我们可以假设整个文件在内存中,我们可以简单地将文件当成一个Buffer来处理。下面是一个非常简单的例子。
Java代码:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class LargeMappedFiles {
// The file created with the preceding program is 128 MB long, which is
// probably larger than the space your OS will allow. The file appears to be
// accessible all at once because only portions of it are brought into
// memory, and other parts are swapped out. This way a very large file (up
// to 2 GB) can easily be modified. Note that the file-mapping facilities of
// the underlying operating system are used to maximize performance.
static int length = 0x8FFFFFF; // 128 Mb
public static void main(String[] args) throws Exception {
// To do both writing and reading, we start with a RandomAccessFile.
// Note that you must specify the starting point and the length of the
// region that you want to map in the file; this means that you have the
// option to map smaller regions of a large file.
MappedByteBuffer out = new RandomAccessFile("test.dat", "rw")
.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);
for (int i = 0; i < length; i++)
out.put((byte) 'x');
System.out.println("Finished writing");
for (int i = length / 2; i < length / 2 + 6; i++)
System.out.print((char) out.get(i));
}
}
下面给出一个内存映射和传统流读写速度比较的例子。
Java代码:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class MappedIO {
private static int numOfInts = 4000000;
private static int numOfUbuffInts = 200000;
private abstract static class Tester {
private String name;
public Tester(String name) { this.name = name; }
public long runTest() {
System.out.print(name + ": ");
try {
long startTime = System.currentTimeMillis();
test();
long endTime = System.currentTimeMillis();
return (endTime - startTime);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public abstract void test() throws IOException;
}
private static Tester[] tests = {
new Tester("Stream Write") {
public void test() throws IOException {
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(new File("temp.tmp"))));
for(int i = 0; i < numOfInts; i++)
dos.writeInt(i);
dos.close();
}
},
new Tester("Mapped Write") {
public void test() throws IOException {
FileChannel fc =
new RandomAccessFile("temp.tmp", "rw")
.getChannel();
IntBuffer ib = fc.map(
FileChannel.MapMode.READ_WRITE, 0, fc.size())
.asIntBuffer();
for(int i = 0; i < numOfInts; i++)
ib.put(i);
fc.close();
}
},
new Tester("Stream Read") {
public void test() throws IOException {
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("temp.tmp")));
for(int i = 0; i < numOfInts; i++)
dis.readInt();
dis.close();
}
},
new Tester("Mapped Read") {
public void test() throws IOException {
FileChannel fc = new FileInputStream(
new File("temp.tmp")).getChannel();
IntBuffer ib = fc.map(
FileChannel.MapMode.READ_ONLY, 0, fc.size())
.asIntBuffer();
while(ib.hasRemaining())
ib.get();
fc.close();
}
},
new Tester("Stream Read/Write") {
public void test() throws IOException {
RandomAccessFile raf = new RandomAccessFile(
new File("temp.tmp"), "rw");
raf.writeInt(1);
for(int i = 0; i < numOfUbuffInts; i++) {
raf.seek(raf.length() - 4);
raf.writeInt(raf.readInt());
}
raf.close();
}
},
new Tester("Mapped Read/Write") {
public void test() throws IOException {
FileChannel fc = new RandomAccessFile(
new File("temp.tmp"), "rw").getChannel();
IntBuffer ib = fc.map(
FileChannel.MapMode.READ_WRITE, 0, fc.size())
.asIntBuffer();
ib.put(0);
for(int i = 1; i < numOfUbuffInts; i++)
ib.put(ib.get(i - 1));
fc.close();
}
}
};
public static void main(String[] args) {
for(int i = 0; i < tests.length; i++)
System.out.println(tests[i].runTest());
}
}
要注意的是,MappedByteBuffer中的内容随时都有可能发生变化,例如当本程序或其它程序改变了相应的文件时,当这种变化发生时,产生的后果是操作系统相关的,因此很难说明。并且MappedByteBuffer的全部或者一部分可能在任意时刻变成不可达的,比如文件被删除了一部分。当访问一个不可达的区域时,有可能会在当时或之后会抛出未知错误。所以,安全地使用MappedByteBuffer,可以使用下面介绍的FileLock来将映射文件的一件部分锁住, 这样别的线程就无法修改这部分文件了。
FileLock
如果一个线程操作一个文件时不希望其他线程进行访问,则可以通过FileLock锁定一个文件。此类的对象需要依靠FileChannel进行实例化操作。锁定文件有两种方式:共享锁和独占锁。共享锁允许多个线程进行文件的读取操作,独占锁只允许一个线程进行文件的读写操作。下面的是FileLock的示例用法。
Java代码:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
public class FileLockDemo {
public static void main(String[] args) throws Exception {
// Write something to a file for test,make sure e://in.txt is not
// exist,or change the filename to another.
// To simplify the demo,I don't deal the possible error.
File file = new File("in.txt");
FileOutputStream output = new FileOutputStream(file, true);
FileChannel fout = output.getChannel();
ByteBuffer srcs = ByteBuffer.allocate(1024);
srcs.put("Something.".getBytes());
srcs.flip();
fout.write(srcs);
srcs.clear();
// Now try to lock the file.
FileLock lock = fout.tryLock();
if (lock != null) {
System.out.println("File " + file.getName() + " is locked.");
// New a thread to access the file.
new Thread() {
@Override
public void run() {
File file = new File("in.txt");
try {
FileInputStream fis = new FileInputStream(file);
// This may throw a IOException.
fis.read();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(10000);
// Release the file's lock.
lock.release();
System.out.println("File " + file.getName()
+ "'s lock is released.");
}
fout.close();
output.close();
}
}
我们可以使用FileLock来更安全地使用文件内存映射。下面的例子使用了两个线程,每个线程锁定了文件的一部分。
Java代码:
// Locking portions of a mapped file.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class LockingMappedFiles {
static final int LENGTH = 0x8FFFFFF; // 128 Mb
static FileChannel fc;
public static void main(String[] args) throws Exception {
fc = new RandomAccessFile("test.dat", "rw").getChannel();
MappedByteBuffer out = fc
.map(FileChannel.MapMode.READ_WRITE, 0, LENGTH);
for (int i = 0; i < LENGTH; i++)
out.put((byte) 'x');
new LockAndModify(out, 0, 0 + LENGTH / 3);
new LockAndModify(out, LENGTH / 2, LENGTH / 2 + LENGTH / 4);
}
private static class LockAndModify extends Thread {
private ByteBuffer buff;
private int start, end;
LockAndModify(ByteBuffer mbb, int start, int end) {
this.start = start;
this.end = end;
mbb.limit(end);
mbb.position(start);
buff = mbb.slice();
start();
}
public void run() {
try {
// Exclusive lock with no overlap:
FileLock fl = fc.lock(start, end, false);
System.out.println("Locked: " + start + " to " + end);
// Perform modification:
while (buff.position() < buff.limit() - 1)
buff.put((byte) (buff.get() + 1));
fl.release();
System.out.println("Released: " + start + " to " + end);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}