Work Better Than Yesterday!

zhangge's stupid and messy life


Home| Life| Technique Concentrate On One Thing.

Java系列之IO系统及NIO小记

17 Jun 2013

这个是学习Java编程思想上面的Java nio笔记,这个应该有点像异步IO吧,以后再学习。这里总结了一下学习的笔记。

1. Java的IO系统

学过操作系统以后,知道文件系统的文件,其实就是字节集合。IO系统就是input,output系统,对文件系统进行输入和输出的操作。当然,泛化以后也是指网络流的。对IO的操作,可以进行很多很多的封装,例如:按字符,按行,随机存储,二进制,缓冲等等,于是jdk的类库就给我们提供了许许多多的便利。关于java io系统的整体关系图,google一下,有很多了,这里就不粘贴了。

2. File类

这个类应该是我们最常用到的了,即可以代替一个目录,也可代替一个文件;所以既可以判断一个文件是否存在,文件长度,又可以获取目录的子文件。

3. 字节流

最原始的操作方式,也是最灵活的了。顾名思义,就是对字节进行操作。两个顶级接口分别是InputStreamOutputStream。直接在IDE是看这两个接口有哪些子类和实现类即可。比较常用的就是FileInputStreamGZipInputStreamByteArrayInputStreamObjectInputStream了,同样的Output接口分别对应于outputstream的。

4. 字符流

这是以字符的形式进行操作的,很方便的对文本进行操作,不过要注意编码方式。两个顶级的接口是ReaderWriter。同样查看IDE就可以查看实现类和子类。比较常用的是BufferedReader, InputStreamReader, FileReader,同样的Writer接口对应于writer。

5. RandomAccessFile

这是独立的一个类,它适用于大小已知的文件,因为它可以随意seek到一个位置,然后读取和写入文件,不过在初始化的时候就要指定wr权限。这点和C一样的。

6. 新IO系统

上面的笔记都是很简单的,也是因为比较简单,重点是学习下面的nio。代码在Bitbucket上JavaTextBox项目的io包下。

nio就是new io的意思,是java1.4引入的。nio的引入是为了提高速度,实际上,旧的IO包已经使用nio重新实现过了,因此,即使我们不显式地用nio编写代码,也能从中受益。所谓新的IO系统,其实就是采用了更接近于操作系统执行IO的方式:通道(Channel)和缓冲器(Buffer)。可以这样子理解,文件就是一个矿藏,我们要挖采它就需要开取一条通道,就像火车道一样,最后用一个卡车来运输,卡车就是缓冲器。通道是一种相当基础的东西:可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。我们只需要对缓冲器进行操作即可,就像在使用c来操作文件一样。

6.1 获取通道Channel

首先,必须打开文件流,才能够获取到通道。并没有使用新的接口来产生通道,而是使用了已有的字节操作流来获取通道,分别是FileInputStream、FileOuputStream和RandomAccessFile,这三个类是操作字节的,性质上是一样的,所以并没有矛盾。

FileChannel fc = new FileOutputStream("data.txt").getChannel();
fc.close();

fc = new RandomAccessFile("data.txt", "rw").getChannel();
fc.position(fc.size());
fc.close();

fc = new FileInputStream("data.txt").getChannel();
fc.close();

虽然FileChannel由这三个字节操作流获取,并不代表通道是由这三个字节操作流实现,它们是分别独立的实现。注意到,不管是否用RandomAccessFile获取通道,都是可以调用fc.position()来移动位置的,但是RandomAccessFile拿到的通道是既能读又能写。

6.2 获取基本的缓冲器ByteBuffer

对于nio,我们最主要的操作是在缓冲器这里,所以肯定不止一种缓存器,但是最基本的就是ByteBuffer了。nio的目标是快速移动大量数据,所以缓冲器的大小设置很重要,这个需要实际运行的系统决定。我们可以通过下面三种方法获得一个缓冲器:

int BSIZE = 1024;//byte
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
buff = ByteBuffer.allocateDirect(BSIZE);
buff = ByteBuffer.wrap("Some text".getBytes());

allocateDirect比allocate的速度更快,因为与操作系统高度耦合了,但是开支也会很大,而且是根据不同的操作系统而不同。wrap是直接把一个数组包装成为一个缓冲器,然后这个数组其实就是存储器了。

6.3 操作缓冲器ByteBuffer

缓冲器就是一个容器,所以对它的操作无非就是get和put,而这个是最基层的缓冲器,即是字节为单位的,所以,put和get的都是字节或者字节数组:

byte a = 27;
buff.put(a);
byte b = buff.get();

实际上,这两个方法是有很多重载的,可以操作各种类型的字节和buffer。

6.4 缓冲器和通道的交互

卡车装满了以后,就要送进通道,系统就会去写入文件;或者,准备好了卡车,送进通道,系统就会从文件中把内容写入缓冲器。调用的接口是FileChannel的read和write,如下:

fc.write(buff);
int result = fc.read(buff);

注意到的是,如果read返回的是-1便结束了,这无疑是unix和c的分界符,如果我们在unix下用c写过代码的话。

如果要把一个文件直接写入到另外一个文件,可以直接使用transfer的接口:

fcInput.transferTo(0, fcInput.size(), fcOutput);
//或者这样
fcOutput.transferFrom(fcInput, 0, fcInput.size());

6.5 缓冲器的详细API

上面虽然知道了基本的使用,但是,实际上缓冲器的细节还是有点复杂的,不了解的话,是不能好好的使用的。实际上,缓冲器就是一个数组,但是它有四个关键索引:position位置,limit界限,capacity容量,mark标记。

  • position是当前移动的指针的位置,每次调用get,或者put方法的时候,指针都会往后移动,至于移动多少个字节要根据get和put操作的字节数。

  • capacity是这个缓冲器的容量,即大小,能存多少数据。

  • limit是对缓冲器进行操作的时候的限制。如果对缓冲器进行写的时候,我们先调用clear()来清空数据,这时候limit被设置为capacity。如果对缓冲器进行读的时候,我们先调用flip()来准备读取,这时候limit被设置为position,即限制指向了写入时的最后一个数据位置,然后再把position设置为0。

  • mark是一个辅助的功能,用于标记位置。调用mark()的时候会把mark设置为当前的position,当我们调用reset()的时候就会把position设置为mark。

详细的api如下:

capacity();//返回缓冲器的容量
//清空缓冲器为可写状态,position=0,limit=capacity, mark标记被清空。如上所述;实际上并不会抹掉数据,因为你去写数据就会覆盖
clear();
flip();//准备缓冲器为可读状态,limit=position, position=0。如上所述
limit();//返回limit值
limit(int limit);//设置limit
mark();//mark=position;如上所述
position();//返回position;
position(int pos);//设置position
remaining();//返回(limit-position)
hasremaining();//若有有介于position和limit之间的元素,则返回true
rewind();//倒回重新读取数据,position=0,mark标记被清空。区别于clear,是不会重新设置limit的。
//从position位置开始新创建一个缓冲器,mark为定义,limit和capacity为剩余的大小。
//但是元素并没有复制多一份,而是共享的,一旦修改都会互相影响的。
slice();

6.6 关于字节存放的顺序

学过操作系统和网络,都知道字节存放顺序的,一般就是大端和小端。大端(big end)就是高位优先,将数的高位存放在低位地址的存储器上;小端(little end)是地位优先,将地位数存放低位地址的存储器上。注意的是,左边的是低位地址的存储器,右边是高位地址的存储器,刚好和我们想的相反了。例如:

00000000 01100001,这是两个字节的数,从左往右数,分别是低位-高位存储器。如果用大端方式读取,就是97;如果用小端方式读取,就是24832了,即实际的二进制是01100001 00000000

对ByteBuffer设置字节顺序的接口是:

buff.order(ByteOrder.BIG_ENDIAN);
buff.order(ByteOrder.LITTLE_ENDIAN);

注意到的是一个存储器一般都是存储一个字节的,所以,这个字节顺序基本没有影响,但是如果是需要多个字节存储的数字的时候就有影响了。

6.7 视图缓冲器

上面用到的都是最基本的字节缓冲器,是比较底层的一些操作,而实际上,java在上层封装了很多基本数据类型的视窗来查看ByteBuffer的。对int,float,long,double,char,short分别有对应的缓冲器。只要在获取得到ByteBuffer以后调用asXXX的方法就能获取得到相应的视图缓冲器了,或者调用视图缓冲器的wrap方法,如下图所示:

alt text

需要注意两个问题,一是ShortBuffer调用put方法的时候,可能需要考虑到转型。二是编码的问题,如果是用ByteBuffer写的数据,然后用CharBuffer读取,则会出现乱码,需要用Charset来decode,或者直接用CharBuffer来写再读就不会乱码,如下例子所示:

FileChannel fc = new FileOutputStream("data2.txt").getChannel();
fc.write(ByteBuffer.wrap("some text".getBytes()));
fc.close();

//方法一
fc = new FileInputStream("data2.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
fc.read(buff);
buff.flip();
//这个会得到乱码
System.out.println(buff.asCharBuffer());
buff.rewind();//回到文件的起始位置
//解码
String encoding = System.getProperty("file.encoding");
System.out.println("Decode using " + encoding + ": " + Charset.forName(encoding).decode(buff));

//方法二
//写入文件的时候就以编码的形式写
fc = new FileOutputStream("data2.txt").getChannel();
fc.write(ByteBuffer.wrap("some text".getBytes("UTF-16BE")));
fc.close();

fc = new FileInputStream("data2.txt").getChannel();
buff.clear();
fc.read(buff);
buff.flip();
System.out.println(buff.asCharBuffer());

//方法三
//直接使用charbuffer来写进去
fc = new FileOutputStream("data2.txt").getChannel();
buff = ByteBuffer.allocate(24);
buff.asCharBuffer().put("some text");
fc.write(buff);
fc.close();

6.7 内存映射文件

以上的nio基本够用,性能也有所提高,但是,映射文件文件访问是会更加显著的提高速度。内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。MappedByteBuffer是继承自ByteBuffer,获取的方法是调用channel的map(),如下:

int length = 0x8FFFFFF;//128M,实际可以更大

MappedByteBuffer out = new RandomAccessFile("test.data", "rw")
							.getChannel()
							.map(FileChannel.MapMode.READ_WRITE, 0, length);

MappedByteBuffer几乎具有所有视图缓冲器的方法,所以操作极其方便,最大的区别是,对MappedByteBuffer操作完了以后,是不需要通过filechannel来read和write的,因为是filechannel映射过来的。但是,映射文件中的所有输出必须使用RandomAccessFile。

6.8 文件加锁

学过操作系统,线程互斥的知识,就知道多线程的时候锁问题了。在这里,实际上是调用FileChannel来对文件加锁的。SocketChannel,DatagramChannel, ServerSocketChannel不需要加锁,因为他们是从单进程实体继承而来的,我们通常不在两个进程之间共享网络socket。如果,竞争同一文件的线程不在同一个java虚拟机上,或者,一个是java的线程,一个是操作系统的本地线程,这种问题,能否锁住?肯定是可以的,这里的文件锁和java线程那边的锁不一样,这个文件锁对其他的系统进程是可见的,java的文件加锁是直接映射到操作系统的加锁工具的。

FileOutputStream fos = new FileOutputStream("file.txt");
FileLock fl = fos.getChannel().tryLock();
if (fl != null) {
	System.out.println("Locked File");
	TimeUnit.MILLISECONDS.sleep(100);
	fl.release();
	System.out.println("Released Lock");
}
fos.close();

tryLock()方法是非阻塞的,所以有可能获取不到锁,因为被别的线程锁了;而lock()方法是阻塞的,直接等待获取到锁为止。退出临界区要调用release()方法来释放锁。锁可以是独占的,也可以是共享的,这个要根据操作系统是否支持,可以调用FileLock.isShared()进行查询。

也可以对文件进行部分的锁,调用的方法如下:

tryLock(long position, long size, boolean shared);
lock(long position, long size, boolean shared);
  

Sunday don't come easily! Subscribe to RSS Feed