Java编程思想笔记0x0e

Java I/O 系统(四)

新I/O

  • java.nio.*包中引入了新的Java I/O类库,其目的在于提高速度。而速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。在交互过程中只需要和缓冲器交互,把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。
  • 唯一直接与通道交互的缓冲器是ByteBuffer,可以存储未加工字节的缓冲器。
  • 引入NIO后,FileInputStreamOutputStreamRandomAccessFile被修改,可以产生FileChannel,用于操纵字节流。java.nio.channels.Channels类提供了可以在通道中产生ReaderWriter等字符模式类的方法。
public class Test {
    public static void main(String[] args) throws IOException {
        // FileChannel fc = new FileOutputStream("test.out").getChannel();
        FileOutputStream fos = new FileOutputStream("test.out");
        FileChannel fc = fos.getChannel();
        fc.write(ByteBuffer.wrap("Some text".getBytes()));
        fc.close();
        fos.close();
        // fc = new RandomAccessFile("test.out", "rw").getChannel();
        RandomAccessFile raf = new RandomAccessFile("test.out", "rw");
        fc = raf.getChannel();
        fc.position(fc.size());
        fc.write(ByteBuffer.wrap("Some more".getBytes()));
        fc.close();
        raf.close();
        // fc = new FileInputStream("test.out").getChannel();
        FileInputStream fis = new FileInputStream("test.out");
        fc = fis.getChannel();
        ByteBuffer buff = ByteBuffer.allocate(1024);
        fc.read(buff);
        buff.flip();
        while(buff.hasRemaining())
            System.out.print((char)buff.get());
        fc.close();
        fis.close();
    }
}
  • getChannel()会产生一个FileChannel,可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。
  • 对于只读访问,必须显式地使用静态allocate()方法来分配ByteBufferByteBuffer的大小关乎I/O的速度。使用allocateDirect()可以产生与操作系统有更高耦合性的直接缓冲器,以获得更高的速度。但是这种分配的开支会更大,并且具体实现与操作系统相关。
public class ChannelCopy {
    private static final int BSIZE = 1024;
    public static void main(String[] args) throw Exception {
        if(args.length !=2) {
            System.out.println("arguments: sourcefile destfile");
            System.exit(1);
        }
        FileInputStream in = new FileInputStream(args[0]);
        FileOutputStream out = new FileOutputStream(args[1]);
        FileChannel fi = in.getChannel(), fo = out.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
        while((in.readf(buffer)) != -1) {
            buffer.flip();
            fo.write(buffer);
            buffer.clear();
        }
    }
}
  • 一旦调用read()来告知FileChannelByteBuffer存储字节,就必须调用缓冲器上的flip(),准备好让其它对象来读取其内容。如果打算继续使用缓冲器执行read(),则必须使用clear()
  • FileChannel#read()返回-1时即达到了输入的末尾。
  • transferTo()transferFrom()可以将一个通道和另一个通道相连。

转换数据

public class Test {
    public static void main(String[] args) throws IOException {
        // 直接输入输出
        FileOutputStream fos = new FileOutputStream("data2.txt");
        FileChannel fc = fos.getChannel();
        fc.write(ByteBuffer.wrap("Some text".getBytes()));
        fc.close();
        fos.close();
        FileInputStream fis = new FileInputStream("data2.txt");
        fc = fis.getChannel();
        ByteBuffer bb = ByteBuffer.allocate(1024);
        fc.read(bb);
        bb.flip();
        System.out.println(bb.asCharBuffer());
        System.out.println("-----");
        bb.rewind();
        // 输出时解码
        String encoding = System.getProperty("file.encoding");
        System.out.println("encoding: " + encoding);
        System.out.println(Charset.forName(encoding).decode(bb));
        System.out.println("-----");
        fis.close();
        // 输入时编码
        fos = new FileOutputStream("data2.txt");
        fc = fos.getChannel();
        fc.write(ByteBuffer.wrap("Some text".getBytes("UTF-16BE")));
        fc.close();
        fos.close();
        fis = new FileInputStream("data2.txt");
        fc = fis.getChannel();
        bb.clear();
        fc.read(bb);
        bb.flip();
        System.out.println(bb.asCharBuffer());
        // 使用CharBuffer输出
        fos = new FileOutputStream("data2.txt");
        fc = fos.getChannel();
        bb = ByteBuffer.allocate(24);
        bb.asCharBuffer().put("Some text");
        fc.write(bb);
        fc.close();
        fis.close();
        // 使用CharBuffer输入
        fis = new FileInputStream("data2.txt");
        fc = fis.getChannel();
        bb.clear();
        fc.read(bb);
        bb.flip();
        System.out.println(bb.asCharBuffer());
        fc.close();
        fis.close();
        fos.close();
    }
}
/* Output:
卯浥⁴數
-----
encoding: UTF-8
Some text
-----
Some text
Some text
*/
  • java.nio.CharBuffer有一个toString()方法,可以返回一个包含缓冲器所有字符的字符串。但是缓冲器中容纳的是普通的字节,要将其转为字符,要么在输入时对其进行编码,要么在将其从缓冲器输出时对其进行解码。

获取基本类型

  • ByteBuffer插入基本类型数据,可以利用asCharBuffer()asShortBuffer()等获得该缓冲器上的视图,然后使用视图的put()方法。注意使用ShortBufferput()方法时需要进行强制类型转换,而其它所有的数据缓冲器在使用put()方法时,不要进行转换。

视图缓冲器

  • 视图缓冲器可以通过某个特定的基本数据类型的试穿查看其底层的ByteBuffer。此外还可以从ByteBuffer一次一个或者成批(数组)读取基本类型值。
public class IntBufferDemo {
    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.allocate(1024);
        IntBuffer ib = bb.asIntBuffer();
        ib.put(new int[]{11, 42, 47, 99, 143, 811, 1016});
        System.out.println(ib.get(3));
        System.out.println("-----");
        ib.put(3, 1811);
        // [11, 42, 47, 1811, 143, 811, 1016]
        ib.flip();
        while(ib.hasRemaining()) {
            int i = ib.get();
            System.out.println(i);
        }
    }
}
  • 上面代码中,先用重载后的put()方法存储一个整数数组。get()put()方法调用直接访问底层ByteBuffer中的某个整数位置。

小端:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
大端:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

  • ByteBuffer默认是大端形式存储数据,并且数据在网络中传送时也常常使用大端模式。可以使用带有参数ByteOrder.BIG_ENDIANByteOrder.LITTLE_ENDIANorder()方法改变ByteBuffer的字节排序方式。

缓冲器的细节

  • Buffer由数据和四个索引组成。四个索引markpositionlimitcapacity可以高效地访问及操纵Buffer中的数据。
方法 功能
capacity() 返回缓冲区的容量capacity
clear() 清空缓冲区,将position设置为0,limit设置为容量capacity,可以用于覆写缓冲区
flip() limit设为positionposition设为0,用于准备从缓冲区读取已经写入的数据
limit() 返回limit
limit(int lim) 设置limit
mark() mark设为position
position() 返回position
position(int pos) 设置position
remaining() 返回limit - position
hasRemaining() 若有介于positionlimit之间的元素,则返回true

内存映射文件

  • 内存映射文件可以创建和修改因为过大而不能放入内存的文件。对应类为MappedByteBuffer
public class LarggeMappedFiles {
    static int length = 0x8FFFFFFF;
    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("test.dat", "rw");
        MappedByteBuffer mbb = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);
        for (int i = 0; i < length; i++)
            mbb.put((byte)'x');
        System.out.println("Finished writing");
        for (int i = length / 2; i < length / 2 + 6; i++)
            System.out.println((char)mbb.get(i));
    }
}

MappedByteBuffer主要是通过FileChannel#map方法,把文件映射到虚拟内存,并返回逻辑地址address,后续文件的读写操作都使用维护的虚拟内存地址和偏移进行操作。

文件加锁

  • 文件加锁机制允许同步访问某个作为共享资源的文件,并且文件锁对其它的操作系统进程是可见的,因为Java的文件加锁直接映射到了本地操作系统的加锁工具。
public class FileLocking {
    public static void main(String[] args) throws Exception {
        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();
    }
}
  • 通过对FileChannel调用tryLock()lock,就可以获得整个文件的FileLocktryLock()是非阻塞式的,会尝试一次获得锁,如果失败则直接返回。lock()是阻塞式的,会阻塞进程直到获得锁,或调用lock()的线程中断,或调用lock()的通道关闭。Java虚拟机会自动释放锁或者关闭加锁的通道,也可以显式使用FileLock#release()释放锁。
  • 可以对文件的一部分上锁,使用重载的tryLock(long position, long size, boolean shared)lock(long position, long size, boolean shared),其中加锁区域由size - position决定,第三个参数指明是否为共享锁。
  • 对独占所或者共享锁的支持必须由底层的操作系统提供,如果操作系统不支持共享锁并为每一个请求都创建一个锁,那么就会使用独占锁。
  • SocketChannelDatagramChannelServerSocketChannel不需要加锁,因为这些通道是从单进程实体继承而来,通常不在两个进程之间共享网络socket。
  • 对于映射文件,同样可以进行部分加锁,以便其它进程可以修改文件中未被加锁的部分,例如数据库。其方法和上述过程相同。
Author: SinLapis
Link: http://sinlapis.github.io/2019/07/14/Java编程思想笔记0x0e/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.