Java1.4之后提供了java.nio包,里面包含了新的I/O库,通过使用这个nio包可以提高速度。速度的提高来自于,它的结构接近操作系统执行I/O的方式,它的核心操作源自于通道和缓冲器。通道是和I/O系统内部交互的地方,所有的数据都要通过缓冲器交给通道,通道会自动帮你对底层进行存储或取出。而且,唯一直接能够与通道交互的缓冲器是ByteBuffer。
ByteBuffer存入Stream
旧的I/O系统中,有三个类——FileInputStream、FileOutputStream和RandomAccessFile,可以生成FIleChannel。Reader和Writer这种字符模式的类不能产生通道,但是java.nio.channels.Channels类提供了实用方法,用以在通道中产生Reader和Writer。
public class GetChannel {
public static final int BSIZE = 1024;
public static final String File = "text.txt";
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
FileChannel fc = new FileOutputStream("data.txt").getChannel();
fc.write(ByteBuffer.wrap("Some Test".getBytes()));
fc.close();
fc = new RandomAccessFile(File, "rw").getChannel();
fc.position(fc.size());
fc.write(ByteBuffer.wrap("Some More".getBytes()));
fc.close();
fc = new FileInputStream(File).getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
fc.read(buff);
buff.flip();
while(buff.hasRemaining()){
System.out.print((char)buff.get());
}
}
}
对于只读访问,必须显式的使用allocate方法来分配ByteBuffer,如果对于速度有着更高的要求时,可以采用allocateDirect方法,以产生一个与操作系统有更高的耦合性的“直接”缓冲器,但是这种分配的开支会更大,并且具体实现也随操作系统不同而不同,因此必须再次实际运行程序来查看直接缓冲是否可以使我们获得速度上的优势。
以上方法中flip方法表示准备缓冲器以便它的由write提取。Write操作之后信息仍在缓冲器中,此时会是使用clear方法对内部指针重新安排,以便缓冲器在另一个read操作期间能够做好接收数据的准备。
另外,transferTo和TransferFrom方法可以允许我们将一个通道和另一个通道直接相连。
public class TransferTo {
public static final String TRANSFERTO_IN = "TransferTo.java";
public static final String TRANSFERTO_OUT = "TransferTo.out";
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
FileChannel in = new FileInputStream(TRANSFERTO_IN).getChannel(),
out = new FileOutputStream(TRANSFERTO_OUT).getChannel();
in.transferTo(0, in.size(), out);
}
}
基本类型写入ByteBuffer
缓冲器容纳的是普通字节,为了把它们转换成字符,我们要么在输入它们的时候对其进行编码,要么在将其从缓冲器输出时对它们进行解码。在nio中,Charset类可以实现解码功能(Charset.forName(encoding).decode(buff),其中buff是ByteBuffer)。
Bytebuffer只可以保存字节类型的数据,但是它具有可以从其所容纳的字节中产生出各种不同基本类型的值的方法(使用buff.asIntBuffer().put(123)这样的asXXXBuffer方法)。
bf.asIntBuffer().put(99471142);
System.out.println(bf.getInt());
bf.rewind();
除了像上面每次只存储进一个int或者char之类的数据的方法外,ByteBuffer还能够一次存入成批(使用数组)的基本数据类型的值。
public class IntBufferDemo {
private static final int BSIZE = 1024;
public static void main(String[] args) {
ByteBuffer bf = ByteBuffer.allocate(BSIZE);
IntBuffer ib = bf.asIntBuffer();
ib.put(new int[] { 11, 42, 47, 99, 143, 811, 1016 });
System.out.println(ib.get(3));
ib.put(3, 1811);
ib.flip();
while (ib.hasRemaining()) {
int i = ib.get();
System.out.println(i);
}
}
}
ByteBuffer默认采用的字节排列顺序是big endian,如果需要改变其字节排列顺序可以使用带有参数ByteOrder.BIG_ENDIAN或ByteOrder.LITTLE_ENDIAN的order方法来改变。
总结以上的过程来看,如果要把数组写到文件中,那么就应该使用ByteBuffer.wrap()方法把字节数组包装起来,然后用getChannel方法在FileOutputStream上打开一个通道,接着将来自于ByteBuffer的数据写入FileChannel中。需要注意的是,ByteBuffer是将数据移入移出通道的唯一方式,并且我们只能创建一个独立的基本类型缓冲器,或者使用“as”方法从ByteBuffer中获得。也就是说我们不能从基本类型的缓冲器(如,IntBuffer)转成ByteBuffer,然而我们可以经视图缓冲器(如,IntBuffer有存入数组的视图方法),将基本数据存入ByteBuffer。
需要额外注意的是Buffer的内部是有四个索引构成的:mark(标记,调用后会记录其索引的位置),position(位置,下一个要读取或写入的元素的索引),limit(限制,第一个不应该读取或写入的元素的索引,不能为负,并且不能大于其容量)和capacity(容量,它所包含的元素的数量)。四者的关系应该满足:0 <= 标记 <= 位置 <= 限制 <= 容量。
内存映射文件
内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。
public class LargeMappedFiles {
private static final int LENGTH = 0x8FFFFFF;
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
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("Finish writing!");
for(int i = LENGTH /2; i < LENGTH /2 +6;i++){
System.out.println((char)out.get(i));
}
}
}
为了既能写,又能读,我们由RandomAccessFile开始,获得该文件的通道,然后调用map产生MappedByteBuffer,这是一种特殊类型的直接缓冲器,可以通过对它设定长度和起始位置来映射一个大文件中的较小部分。MappedByteBuffer由ByteBuffer继承而来,因此它具有ByteBuffer的所有方法。使用MappedByteBuffer可以帮助我们很容易的访问很大的文件。
文件加锁
文件加锁机制允许我们同步访问某个作为共享资源的文件,不过这种竞争可以是两个线程,也可能是不同虚拟机,或者一个是Java线程,另一个是操作系统的某个本地线程。文件加锁对其他的操作系统线程是可见的,因为Java的文件加锁直接映射到了本地操作系统的加锁工具。
public class FileLocking {
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream("file.txt");
FileLock lock = fos.getChannel().tryLock();
if(lock != null){
System.out.println("Locked File");
TimeUnit.MILLISECONDS.sleep(100);
lock.release();
System.out.println("Released Lock!");
}
fos.close();
}
}
Thinking in Java中的Stream和Channel以及Buffer的关系
对于SocketChannel、DatagramChannel,ServerSocketChannel来说不需要加锁,因为它们是从单线程实体继承而来,我们通常不在两个线程之间共享网络Socket。tryLock是非阻塞的,它设法获取锁,但是如果不能获得,它将直接从调用返回。Lock则是阻塞式的,它要阻塞进程直至可以获得,或调用lock的线程中断,或调用lock的通道关闭。
当然也可以对文件的部分加锁,使用tryLock(long position, long size, Boolean shared)和try(long position, long size, Boolean shared)两个方法,可以对position到position+size的区域加锁,当文件增大超出position+size时,那么position+size之外的部分不会被锁定。无参数的加锁方法会对整个文件进行加锁,甚至文件变大后也是如此。最后要说的是锁的类型可以通过FileLock.isShared()进行查询。