Java—NIO

Java—NIO

NIO既被人称为New IO,也被人称为Non-blocked IO(非阻塞式),不管名字叫什么,我们都要了解它的特性和用法

JDK1.4引入了NIO这个库,NIO和IO有相同的作用和目的,但实现方式不同

1.为什么要使用NIO?

传统IO是基于字节的,所有IO都被视为单个字节的移动,而NIO是基于块的,每次移动一大块数据,所以性能肯定优于IO

  • NIO性能提高是因为它使用的结构更接近操作系统执行IO的方式:通道和缓冲器

我们可以把它想象成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲器是被派送到矿藏的卡车。卡车载满煤炭而归,我们再从卡车上获得煤炭。也就是说,我们没有直接和通道交互;我们只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据(源自《Java编程思想》)


2.NIO基础

Buffer和Channel是标准NIO中的核心对象,下面介绍这两个工具

1> Channel

Channel是一个对象,可以通过它读取和写入数据,可以把它看作IO中的流,但它和流还有一些不同:

  • Channel是双向的,既可以读又可以写。而流是单向的
  • Channel可以进行异步的读写
  • 对Channel的读写必须通过Buffer

上面提到的,所有数据都通过Buffer对象处理,所以你不会直接把字节写入到Channel中。相反,你是将数据写到Buffer中;同样你也不会直接从Channel中读取字节,而是将数据从Channel读入Buffer,再从Buffer获取数据

NIO中Channel主要有以下几个类:

  • FileChannel:从文件中读写数据
  • DatagramChannel:读写UDP网络协议数据
  • SocketChannel:读写TCP网络协议数据
  • ServerSocketChannel:可以监听TCP连接

2> Buffer

  • Buffer是一个对象,它包含一些要写入或者读取的数据。在NIO中,数据是放入Buffer对象的。而在IO中,数据是直接写入或者读到Stream对象的。程序不能直接对Channel进行读写操作,而必须通过Buffer来进行,即Channel是通过Buffer来读取的

实际上,这个Buffer是一个(被包装过的)字节数组(ByteBuffer),也可以通过调用视图来操作其他类型的数据,但ByteBuffer依然是实际存储数据的地方,在使用Buffer时主要有以下几个步骤

  • 创建ByteBuffer,调用allocate()方法申请一块字节缓冲区
  • 写入数据到Buffer中
  • 对Buffer调用flip()方法,让Buffer转化为准备被读取的状态
  • 从Buffer中读取数据
  • 对Buffer调用clear()方法,让Buffer转化为准备被写入的状态

3.NIO的一些用法

  • 1> 复制文件
public class ChannelCopy {
    public static void main(String[] args) throws IOException {
        FileChannel in = new FileInputStream(new File("d:\\test.txt")).getChannel();
        FileChannel out = new FileOutputStream(new File("d:\\test_02.txt")).getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(12);
        while (in.read(buffer) != -1){
            buffer.flip();
            out.write(buffer);
            buffer.clear();
        }
        in.close();
        out.close();
    }
}

要素:

  • Channel: 我们通过调用输入流和输出流的getChannel()来获取对应的FileChannel对象
  • Buffer: 使用ByteBuffer类中的allocate()方法申请一块缓冲区域

操作:将数据从原文件的Channel中读取到Buffer中,flip()方法告诉Buffer可以进行写操作了,然后再把Buffer中的数据写入到复制文件的Channel中,最后clear()清空缓冲区

  • 2> 视图缓冲器
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));
        ib.put(3,1811);
        ib.flip();
        while (ib.hasRemaining()){
            int i = ib.get();
            System.out.print(i + " ");
        }
    }
}
  • 有时候,我们想用NIO进行读写的数据不只是字节类型,可能时字符类型,整形,浮点型等等…这个时候我们就可以指定相对应的视图缓冲器来对Buffer中的字节数据进行操作(很多博客中把Buffer分为很多类型,但实际上Buffer只有一种,就是ByteBuffer。能对其他类型的缓冲器进行操作,如上面的IntBuffer,就必须用到ByteBuffer中的调用视图缓冲器方法)
  • 上面这个程序就是利用整形视图缓冲器在Buffer中存储了一个整形数组,并且能对其进行读和写的操作。

  • 3> 缓冲器细节

Buffer由数据和可以高效的访问及操作这些数据的四个索引组成,这四个索引是:mark,position,limit,capacity。下面是用于设置和复位索引以及查询他们的值的方法:
  • capacity():返回缓冲区容量
  • clear():清空缓冲区,将position设置为0,limit设置为容量。我们可以用此方法覆写缓冲区
  • flip():将limit设置为position,再把position置为0,此方法用于准备从缓冲区读取已经写入的数据
  • limit():返回limit的值
  • limit(int lim):设置limit的值
  • mark():将mark设置为position
  • position():返回position值
  • position(int pos):设置position值
  • remaining():返回(limit—position)
  • hasRemaining():若有介于position和limit之间的元素,则返回true
  • rewind():把position设置到缓冲器的开始位置

4.内存映射文件

  • 内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。有了内存映射文件,我们就可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组来访问。下面是个例子
public class LargeMappedFiles {
    static int length = 0x8FFFFFF;
    public static void main(String[] args) throws IOException {
        MappedByteBuffer out = new RandomAccessFile("d:\\test.txt","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.println((char)out.get(i));
        }
    }
}
  • Channel对象调用map()方法可获取对应的内存映射文件,这是一种特殊的直接缓冲器,并且必须指定文件文件的初始位置和映射区域的长度,这意味着我们可以映射某个大文件的较小部分

  • MappedByteBuffer由ByteBuffer继承而来,因此它具有ByteBuffer的所有方法。这里我们仅仅展示了get()和put()方法,但是我们同样可以使用像asCharBuffer()这样的用法


5.文件加锁

  • JDK 1.4引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。不过,竞争同一文件的两个线程可能在不同的java虚拟机上;或者一个是java线程,另一个是操作系统中其他的某个本地线程。文件锁对其他的操作系统进程是可见的,因为java文件加锁直接映射到了本地操作系统的加锁工具

下面是一个例子

public class FileLocking {
    public static void main(String[] args) throws IOException, InterruptedException {
        FileOutputStream fos = new FileOutputStream(new File("d:\\test.txt"));
        FileLock fl = fos.getChannel().tryLock();
        if (fl != null){
            System.out.println("Locked File");
            TimeUnit.MILLISECONDS.sleep(1000);
            fl.release();
            System.out.println("Released Lock");
        }
        fos.close();
    }
}

通过对FileChannel调用tryLock()或lock(),就可以获得整个文件的FileLock。

  • tryLock():非阻塞式的,它设法获取锁,但是如果不能获得(当其他一些进程已经持有相同的锁,并且不共享时),它直接从方法调用返回。

  • lock():阻塞式的,他要阻塞进程直到锁可以获得,或者调用lock()的线程中断,或调用lock()的通道关闭。使用FileLock.release()可以释放锁。

  • 也可以使用如下方法对文件的一部分上锁

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

猜你喜欢

转载自blog.csdn.net/wintershii/article/details/81367899