Java NIO核心三大组件Channel、Buffer和Selector(一)

一、BIO、NIO和AIO简介

通常所说的 BIO 是相对于 NIO 来说的,BIO 也就是 Java 开始之初推出的 IO 操作模块。

1、BIO(Blocking I/O)同步阻塞I/O

BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。

优点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。尤其是在网络编程中,瓶颈体现的非常明显!

比如:熟知的Socket编程就是BIO,一个socket连接一个处理线程(这个线程负责这个Socket连接的一系列数据传输操作)。阻塞的原因在于:操作系统允许的线程数量是有限的,多个socket申请与服务端建立连接时,服务端不能提供相应数量的处理线程,没有分配到处理线程的连接就会阻塞等待或被拒绝。

通常我们把 java.net下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。

2、NIO (New I/O) 同步非阻塞I/O

NIO 是 Java 1.4 引入的 java.nio 包,New IO是对BIO的改进,同时支持阻塞与非阻塞模式,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。

关于NIO,国内有很多技术博客将英文翻译成No-Blocking I/O,非阻塞I/O模型 ,当然这样就与BIO形成了鲜明的特性对比。NIO本身是基于事件驱动的思想来实现的,其目的就是解决BIO的大并发问题。

3、AIO (Asynchronous I/O) 异步非阻塞I/O

AIO (Asynchronous I/O) 异步非阻塞I/O 是 Java 1.7 之后的,,在java.nio包,AIO是是 NIO 的升级版本(所以AIO又叫NIO.2),提供了异步非堵塞的 IO 操作方式,异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

 

AIO是发出IO请求后,由操作系统自己去获取IO权限并进行IO操作;NIO则是发出IO请求后,由线程不断尝试获取IO权限,获取到后通知应用程序自己进行IO操作。

IO实质上与线程没有太多的关系,但是不同的IO模型改变了应用程序使用线程的方式,NIO与AIO的出现解决了很多BIO无法解决的并发问题。

4、NIO三大核心组件

在java NIO编程中,需要理解java.nio包下的三大核心组件:Channel、Buffer和Selector。

1)Channel

Channel和IO中的Stream是差不多一个等级的。只不过Stream是单向的,而Channel是双向的,通过它既可以用来进行读操作,又可以用来进行写操作。

Channel是一个接口,继承于Closeable接口,它是数据的源头或者数据的目的地,用于向 buffer 提供数据或者读取 buffer数据。所有需要读取和写入的数据必须都通过Buffer对象来处理。

在JAVA NIO中,提供了多种Channel对象,而所有的通道对象都实现了Channel接口。其中Channel的主要实现有:

FileChannel 是连接到文件的通道

DatagramChannel 是连接到UDP包的通道

SocketChannel 是连接到TCP网络套接字的通道

ServerSocketChannel 是监听新进来的TCP连接的通道

2)Buffer

Buffer是一块连续的内存块。是 NIO 数据读或写的缓冲区。

缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。

在NIO中,所有的缓冲区类型都继承于Buffer抽象类,主要的Buffer实现有:ByteBuffer(最常用)、CharBuffer、DoubleBuffer、 FloatBuffer、IntBuffer、 LongBuffer、ShortBuffer,分别对应基本数据类型: byte、char、double、 float、int、 long、 short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等。

3)Selector

选择器允许单线程操作多个通道。Selector 是NIO相对于BIO实现多路复用的基础,Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。比如聊天服务器。

 

二、Channel组件 - FileChannel的使用

Channel是一个接口,继承于Closeable接口,它是数据的源头或者数据的目的地,这里主要使用FileChannel。

Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。

FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。

FileChannel方法如下,后面涉及的其他类请自行查阅API

FileChannel方法摘要
abstract  void force(boolean metaData)
          强制将所有对此通道的文件更新写入包含该文件的存储设备中。
 FileLock lock()
          获取对此通道的文件的独占锁定。
abstract  FileLock lock(long position, long size, boolean shared)
          获取此通道的文件给定区域上的锁定。
abstract  MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)
          将此通道的文件区域直接映射到内存中。
abstract  long position()
          返回此通道的文件位置。
abstract  FileChannel position(long newPosition)
          设置此通道的文件位置。
abstract  int read(ByteBuffer dst)
          将字节序列从此通道读入给定的缓冲区。
 long read(ByteBuffer[] dsts)
          将字节序列从此通道读入给定的缓冲区。
abstract  long read(ByteBuffer[] dsts, int offset, int length)
          将字节序列从此通道读入给定缓冲区的子序列中。
abstract  int read(ByteBuffer dst, long position)
          从给定的文件位置开始,从此通道读取字节序列,并写入给定的缓冲区。
abstract  long size()
          返回此通道的文件的当前大小。
abstract  long transferFrom(ReadableByteChannel src, long position, long count)
          将字节从给定的可读取字节通道传输到此通道的文件中。
abstract  long transferTo(long position, long count, WritableByteChannel target)
          将字节从此通道的文件传输到给定的可写入字节通道。
abstract  FileChannel truncate(long size)
          将此通道的文件截取为给定大小。
 FileLock tryLock()
          试图获取对此通道的文件的独占锁定。
abstract  FileLock tryLock(long position, long size, boolean shared)
          试图获取对此通道的文件给定区域的锁定。
abstract  int write(ByteBuffer src)
          将字节序列从给定的缓冲区写入此通道。
 long write(ByteBuffer[] srcs)
          将字节序列从给定的缓冲区写入此通道。
abstract  long write(ByteBuffer[] srcs, int offset, int length)
          将字节序列从给定缓冲区的子序列写入此通道。
abstract  int write(ByteBuffer src, long position)
          从给定的文件位置开始,将字节序列从给定缓冲区写入此通道。

在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。

1、打开,读数据,关闭FileChannel

下面是通过RandomAccessFile打开FileChannel的示例:java.io.RandomAccessFile的API使用

RandomAccessFile类支持对随机访问文件的读取和写入。

构造方法摘要
RandomAccessFile(File file, String mode)
          创建从中读取和向其中写入(可选)的随机访问文件流,该文件由 File 参数指定。
RandomAccessFile(String name, String mode)
          创建从中读取和向其中写入(可选)的随机访问文件流,该文件具有指定名称。
FileChannel getChannel()
          返回与此文件关联的唯一 FileChannel 对象。

mode 参数指定用以打开文件的访问模式。允许的值及其含意为:

   -  “r”:以只读的方式打开,调用该对象的任何write(写)方法都会导致IOException异常

   -  “rw”:以读、写方式打开,支持文件的读取或写入。若文件不存在,则创建。

   -  “rws”:以读、写方式打开,与“rw”不同的是,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。这里的“s”表示synchronous(同步)的意思

   -  “rwd”:以读、写方式打开,与“rw”不同的是,还要求对文件内容的每个更新都同步写入到底层存储设备。

使用“rwd”模式仅要求将文件的内容更新到存储设备中。而使用“rws”模式除了更新文件的内容,还要更新文件的元数据(metadata),这通常要求至少一个以上的低级别 I/O 操作。

    public static void main(String[] args) {
        FileChannel channel = null;
        try {
            // 1.创建FileChannel
            RandomAccessFile accessFile = new RandomAccessFile("D:/E/NIO/text.txt", "rw");
            channel = accessFile.getChannel();

            // 缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 2.FileChannel读取数据,将通道中的读取数据到写入缓冲区中
            int readLength = channel.read(buffer);
            while (readLength != -1){
                // 简单理解为,将写入模式改为读取模式
                buffer.flip();
                while (buffer.hasRemaining()){
                    System.out.print((char)buffer.get());
                }
                // 清空当前的缓冲区的数据,缓冲区重复使用
                buffer.clear();
                readLength = channel.read(buffer);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(channel != null){
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

首先,分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。

然后,调用FileChannel.read()方法。该方法将数据从FileChannel读取到Buffer中。read()方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。 

2、打开,写数据和关闭FileChannel 

    public static void main(String[] args) {
        FileChannel channel = null;
        FileOutputStream outputStream = null;
        try {
            String str = "admindadj nio 123123";
            outputStream = new FileOutputStream("D:/E/NIO/text1.txt");

            // 1.获取FileChannel
            channel = outputStream.getChannel();

            // 缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 将数据放到缓冲区中, 这里放了两次
            byte[] data = str.getBytes();
            for (int i = 0; i < data.length; i++) {
                buffer.put(data[i]);
            }
            buffer.put(data);

            // 2.FileChannel写入数据,将缓冲区中的数据写入到通道中
            // 简单理解为,将写入模式改为读取模式
            buffer.flip();
            while (buffer.hasRemaining()) {
                channel.write(buffer);
                System.out.println(channel.size());
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

注意:FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

总结:

通过通道写入文件:代码—>写入(write)缓冲区(buffer)—>通道(Channel)—>目标文件(File)

通过通道读取文件:目标文件(File)—>通道(Channel)读取到(read)—>缓冲区(buffer)—>代码

三、Buffer组件

在NIO中,所有数据都是用缓冲区处理的。缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),Buffer就是经过包装后的一个数组。该对象提供了一组通用api方法对这个数组进行操作,可以更轻松的使用内存块。

1、Buffer属性

Buffer有以下四个属性:提供了关于包含的数据元素的信息。

Capacity (容量):缓冲区能够容纳的数据元素的最大数量。该容量在缓冲区创建时被设定,缓冲区的容量永远不会为负并且永远不能被更改。

Limit (限制/上界):缓冲区的限制是第一个不能被读取或写入的元素的索引。或者说,缓冲区中现存元素的计数。缓冲区的限制永远不会为负,并且永远不会大于其容量。

Position (位置):缓冲区的位置是下一个要读取或写入的元素的索引。位置会自动由相应的get( )和put( )函数更新。缓冲区的位置永远不会为负,并且永远不会大于其限制。

Mark (标记):缓冲区的标记是在调用reset 方法时其位置将被重置到该mark位置。标记在设定前是默认值是-1。或者说一个备忘位置。调用mark( )来设定mark = postion。调用reset( )设定position = mark。

如果定义了标记后,则调用reset方法时,该标记将被丢弃。

如果未定义标记,则调用 reset 方法将导致抛出InvalidMarkException。

这四个属性之间一般总是遵循以下关系:0 <= 标记(mark) <= 位置(position) <= 限制(limit) <= 容量(capacity)

新创建的缓冲区位置为零(position = 0)和标记未定义(mark = -1)。初始限制可以为零,也可以是其他某个值,这取决于缓冲区类型及其构建方式。

当缓冲区创建完毕,则容量就不变,而其他属性随读写等操作改变。

2、缓冲区的一些方法,下面为ByteBuffer写demo 

    • int capacity()

      返回此缓冲区的容量。

      boolean hasRemaining()

      告诉当前位置和极限之间是否有任何元素。

      int remaining()

      返回当前位置和极限之间的元素的数目。

      int position()

      返回此缓冲区的位置。

      Buffer position(int newPosition)

      设置此缓冲区的位置。

      int limit()

      返回此缓冲区的限制。

      Buffer limit(int newLimit)

      设置此缓冲区的限制。

      abstract boolean isDirect()

      告诉这是否是 direct缓冲。

      abstract boolean isReadOnly()

      告诉是否该缓冲区是只读的。

      Buffer mark()

      设置此缓冲区的标记位置。

      Buffer reset()

      重置此缓冲区的位置之前标记的位置。

      Buffer clear()

      清除此缓冲区。

1)间接缓冲区的创建

    • static ByteBuffer allocate(int capacity)

      分配一个新的字节缓冲区。

      static ByteBuffer wrap(byte[] array)

      将一个字节数组封装到一个缓冲区中。

      static ByteBuffer wrap(byte[] array, int offset, int length)

      将一个字节数组封装到一个缓冲区中。position=offset,limit=lenth

      boolean hasArray()

      判断缓冲区底层实现是否为数组

      byte[] array()

      返回缓冲区的字节数组。

      int arrayOffset()

      返回在缓冲  第一元这个缓冲区支持数组的偏移(可选操作)。

通过allocate(分配操作)或者wrap(包装操作)函数创建的缓冲区通常都是间接的。间接的缓冲区使用备份数组。

  • 分配操作创建一个缓冲区对象并分配一个容量大小的私有的空间来储存数据元素。
  • 包装操作创建一个缓冲区对象,但是不分配任何空间来储存数据元素。它使用所提供的数组作为存储空间来储存缓冲区中的数据元素。

如果hasArray()函数返回false,调用array()函数或者arrayOffset()函数时,会抛出UnsupportedOperationException异常。

如果一个缓冲区是只读的,且这个数组对象被提供给wrap()函数。那么调用array()函数或者arrayOffset()会抛出一个ReadOnlyBufferException异常,来阻止您得到存取权来修改只读缓冲区的内容。 

        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        String str = "12345";
        byteBuffer.put(str.getBytes());
        System.out.println(byteBuffer.hasArray()); // true
        byte[] array = byteBuffer.array();
        System.out.println((char)array[1]); // 2
        System.out.println(byteBuffer.arrayOffset()); //0

        byte[] arr = new byte[9];
        ByteBuffer wrap = ByteBuffer.wrap(array);
        System.out.println(byteBuffer.hasArray()); // true
        System.out.println((char)byteBuffer.array()[3]); // 4
        System.out.println(byteBuffer.arrayOffset()); // 0

3)获取和设置属性的方法

    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        int capacity = byteBuffer.capacity();
        int position = byteBuffer.position();
        int limit = byteBuffer.limit();

        System.out.println("=====新建缓冲区=====");
        System.out.println("capacity:" + capacity + ",position:" + position + ",limit:" + limit);

        System.out.println("=====修改position,limit之后,并标记=====");
        byteBuffer.position(5);
        byteBuffer.limit(8);
        // 调用mark( )来设定mark = postion
        Buffer mark = byteBuffer.mark();
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());
        System.out.println("mark:" + mark);

        byteBuffer.position(6);
        byteBuffer.limit(7);
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());
        System.out.println("=====reset标记之后=====");
        // 调用reset( )设定position = mark。
        byteBuffer.reset();
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());
    }

    Java NIO核心三大组件Channel、Buffer和Selector(一)_第1张图片

4)翻转和重置函数

    • Buffer flip()

      翻转这个缓冲区。

      Buffer rewind()

      重置此缓冲区。

flip()方法 翻转此缓冲区

首先将当前位置设置为限制,然后将该位置设置为零。如果已定义了标记,则丢弃该标记。可以理解为:将一个能够继续添加数据元素的状态的缓冲区翻转成一个准备读出元素的释放状态的缓冲区,反之亦然。

    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        String str = "12345";
        byteBuffer.put(str.getBytes());
        System.out.println("=====存入五个字节之后=====");
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());

        System.out.println("=====flip之后=====");
        byteBuffer.flip();
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());
    }

     Java NIO核心三大组件Channel、Buffer和Selector(一)_第2张图片     Java NIO核心三大组件Channel、Buffer和Selector(一)_第3张图片

rewind() 重置此缓冲区:将位置设置为零,并丢弃标记。它与flip()相似,但不影响上界属性。

    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        String str = "12345";
        byteBuffer.put(str.getBytes());
        System.out.println("=====存入五个字节之后=====");
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());

        System.out.println("=====rewind之后=====");
        byteBuffer.rewind();
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());
    }

    Java NIO核心三大组件Channel、Buffer和Selector(一)_第4张图片    Java NIO核心三大组件Channel、Buffer和Selector(一)_第5张图片

5)存值和取值函数

    • abstract byte get()

      获取缓冲区当前位置上的字节,并位置加1

      abstract byte get(int index)

      获取指定索引中的字节

      ByteBuffer put(byte[] src)

      将字节写入当前位置的缓冲区中,并位置加1

      abstract ByteBuffer put(int index, byte b)

      在给定位置放入字节;

         
        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        String str = "12345";
        byteBuffer.put(str.getBytes());

        byteBuffer.flip();
        System.out.println((char)byteBuffer.get());
        System.out.println((char)byteBuffer.get());
        System.out.println((char)byteBuffer.get(3));
        System.out.println((char)byteBuffer.get());
==
1
2
4
3

6)clear() 和compact()

    • abstract ByteBuffer compact()

      缓冲区当前位置和它的极限之间的字节数,如果有的话,被复制到缓冲区的开始。

clear() 清除此缓冲区。将位置设置为零,限制设置为该容量,并且丢弃标记。

注意:此方法并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回0。

        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        String str = "12345";
        byteBuffer.put(str.getBytes());

        System.out.println("=====存入五个字节之后=====");
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());

        System.out.println("=====clear之后=====");
        byteBuffer.clear();
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());

        System.out.println("=====clear之后,还是能拿到数据=====");
        System.out.println((char)byteBuffer.get());
        System.out.println((char)byteBuffer.get());

  Java NIO核心三大组件Channel、Buffer和Selector(一)_第6张图片  Java NIO核心三大组件Channel、Buffer和Selector(一)_第7张图片 

compact()压缩函数:只想从缓冲区中释放一部分数据,而不是全部,然后重新填充到开始位置。有时可以替换 clear()。

在复制数据时要比使用get()和put()函数高效得多。所以当需要时,可以使用compact()。比如:实现读取数据舍弃,未读取的数据元素需要下移至第一个元素位置。

        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        String str = "12345";
        byteBuffer.put(str.getBytes());

        byteBuffer.flip();
        System.out.println((char)byteBuffer.get());
        System.out.println((char)byteBuffer.get());

        System.out.println("=====读取连个个字节之后=====");
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());

        System.out.println("=====compact之后,可写=====");
        byteBuffer.compact();
        byteBuffer.put("8".getBytes());
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());

        System.out.println("=====compact之后,flip 读数据=====");
        byteBuffer.flip();
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());
        while (byteBuffer.remaining() >= 0){
            System.out.print((char)byteBuffer.get());
        }

   Java NIO核心三大组件Channel、Buffer和Selector(一)_第8张图片   Java NIO核心三大组件Channel、Buffer和Selector(一)_第9张图片

7)比较函数

    • boolean equals(Object ob)

      判断缓冲区是否等于另一个对象。

      int compareTo(ByteBuffer that)

      计较缓冲区与另一缓冲区的大小

equals( )函数:用以测试两个缓冲区的是否相等

两个缓冲区被认为相等的充要条件是:

    1.两个对象类型相同。包含不同数据类型的buffer永远不会相等,而且buffer绝不会等于非buffer对象。

    2.两个对象都剩余同样数量的元素。Buffer的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相同。

    3.在每个缓冲区中应被get()函数返回的剩余数据元素序列必须一致。

如果不满足以上任意条件,就会返回false。

compareTo( )函数:用于比较两个缓冲区的大小。

A.compareTo(B),A小于B,A等于B,A大于B分别返回一个负整数,0和正整数。

equals()和compareTo()都不允许不同对象之间进行比较。但compareTo( )更为严格:如果您传递一个类型错误的对象,它会抛出ClassCastException异常,但equals( )只会返回false。

        ByteBuffer byteBuffer1 = ByteBuffer.allocate(9);
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(9);
        byteBuffer1.put("134".getBytes());
        byteBuffer2.put("125".getBytes());
        System.out.println(byteBuffer1.equals(byteBuffer2)); // true
        System.out.println(byteBuffer1.compareTo(byteBuffer2)); // 0

        byteBuffer1.flip();
        byteBuffer2.flip();
        System.out.println(byteBuffer1.equals(byteBuffer2)); // false
        System.out.println(byteBuffer1.compareTo(byteBuffer2)); // 1

 Java NIO核心三大组件Channel、Buffer和Selector(一)_第10张图片 Java NIO核心三大组件Channel、Buffer和Selector(一)_第11张图片

 

3、ByteBuffer(字节缓冲区)独特的几个知识点

ByteBuffer 只是 Buffer 的一个子类,它跟其他缓冲区类型的不同的几个知识点:

1)ByteBuffer 可做为通道所执行的 I/O 的源头或目标。看FileChannel的源码,就会发现文件通道只接收 ByteBuffer 作为参数。

字节是操作系统及其 I/O 设备使用的基本数据类型。

当在 JVM 和操作系统间传递数据时,都会将其他数据类型拆分成构成它们的字节形式进行传递,因为,系统层次的 I/O 都是面向字节的。所以,只有字节缓冲区有资格直接参与 I/O 操作。

2)endian-ness(字节顺序)

多字节数值被存储在内存中的方式一般被称为 endian-ness(字节顺序)。

如果多字节数值的最高字节 - big end(大端),位于低位地址(即 big end 先写入内存,先写入的内存的地址是低位的,后写入内存的地址是高位的),那么系统就是大端字节顺序来存储数据的。反之,系统就是小端字节顺序。

对于常用的CPU架构,如Intel,AMD的CPU使用的都是小端字节顺序。

ByteBuffer类有所不同:默认字节顺序总是 ByteOrder.BIG_ENDIAN。

在java.nio中,字节顺序由ByteOrder类封装。可以通过它改变ByteBuffer类的字节顺序。

        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        System.out.println(byteBuffer.order()); // BIG_ENDIAN

        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
        System.out.println(byteBuffer.order()); // LITTLE_ENDIAN

        // JVM运行的硬件平台的固有字节顺序
        System.out.println(ByteOrder.nativeOrder());// LITTLE_ENDIAN

3)对于不同的字节顺序,在从这个字节序列中批量取出字节的结果会是不一样的。 

        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        byteBuffer.put("12345678".getBytes());
        System.out.println(byteBuffer.order());// BIG_ENDIAN
        byteBuffer.flip();
        System.out.println(byteBuffer.getInt(0));
        int b = byteBuffer.order(ByteOrder.BIG_ENDIAN).getInt(0);
        int l = byteBuffer.order(ByteOrder.LITTLE_ENDIAN).getInt(0);
        System.out.println("b=" + b + ", l=" + l);

    

 

四、视图缓冲区

缓冲区不限于能管理数组中的数据元素,它们也能管理其他缓冲区中的数据元素。当一个管理其他缓冲区所包含的数据元素的缓冲区时,这个缓冲区被称为视图缓冲区。

    • abstract ByteBuffer duplicate()

      创建一个新的字节缓冲区,共享该缓冲区的内容。

      abstract ByteBuffer asReadOnlyBuffer()

      创建一个新的只读字节缓冲区,该缓冲区共享该缓冲区的内容。

      abstract ByteBuffer slice()

      创建一个新的字节缓冲区的内容是一个共享的子缓冲区的内容。

1、Duplicate()函数

Duplicate()函数创建一个与原始缓冲区相似的新缓冲区,相当于复制。

两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。

对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。

如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。

        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        ByteBuffer duplicate = byteBuffer.duplicate();

2、asReadOnlyBuffer()函数

asReadOnlyBuffer() 函数创建一个只读的视图缓冲区。

这与duplicate() 相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly() 函数将会返回true。

如果一个只读的缓冲区与一个可写的缓冲区共享数据或有包装好的备份数组,那么对这个可写的缓冲区或直接对这个数组的改变将反映在所有关联的缓冲区上,包括只读缓冲区(不能put)。

        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.isReadOnly()); // true

3、slice()函数

分割缓冲区与复制相似,但 slice() 创建一个从原始缓冲区的当前 position 开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数量(limit - position)。

这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。

        ByteBuffer byteBuffer = ByteBuffer.allocate(9);
        byteBuffer.put("12345".getBytes());
        byteBuffer.flip();
        byteBuffer.position(2);
        System.out.println("capacity:" + byteBuffer.capacity() + ",position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit());
        ByteBuffer sliceBuffer = byteBuffer.slice();
        System.out.println("capacity:" + sliceBuffer.capacity() + ",position:" + sliceBuffer.position() + ",limit:" + sliceBuffer.limit());
        while (sliceBuffer.remaining() > 0){
            System.out.print((char)sliceBuffer.get());
        }

    

五、直接缓冲区

直接缓冲区会涉及到JVM和操作系统对内存管理的知识。

不同版本的操作系统对物理内存条的划分(在逻辑上的划分),其结果是不一样的。一般情况下,会把内存分为用户空间和系统空间(也可以叫内核空间)。

    Java NIO核心三大组件Channel、Buffer和Selector(一)_第12张图片

用户空间:用户进程所在的内存区域

内核空间:操作系统占据的内存区域。

内核空间(含运行起来的操作系统程序)能一方面与设备控制器(硬件)链接,另一方面控制着用户空间中的进程(如 JVM)的运行状态。

1、直接缓冲区与非直接缓冲区

在Java NIO中,缓冲区对象可以有直接缓冲区与非直接缓冲区之分:

  • 非直接缓冲区:通过 allocate() 或者wrap()方法创建的缓冲区,将缓冲区建立在 JVM 的内存中。
  • 直接缓冲区:通过 allocateDirect() 方法创建的缓冲区,将缓冲区建立在物理内存中。针对大数据处理时,可以提升效率。直接字节缓冲区还可以过通过FileChannel 的 map() 方法将文件区域直接映射到内存中来创建 。通过 MappedByteBuffer操作大文件的方式,其读写性能极高!

1)非直接缓冲区写入步骤:

1.创建一个临时的直接ByteBuffer对象。

2.将非直接缓冲区的内容复制到临时缓冲中。

3.使用临时缓冲区执行低层次I/O操作。

4.临时缓冲区对象离开作用域,并最终成为被回收的无用数据。

    Java NIO核心三大组件Channel、Buffer和Selector(一)_第13张图片

2)直接缓冲区:

在JVM内存外开辟内存,在每次调用操作系统的一个本机I/O之前或者之后,数据传递会少一次复制过程。

如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能。虽然直接缓冲区使JVM可以进行高效的I/O操作,

但它使用的内存是操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓冲区要更大的开销。所以,针对大数据处理时,直接缓冲区可以提升效率。

    Java NIO核心三大组件Channel、Buffer和Selector(一)_第14张图片

注意:allocateDirect()方法创建的直接缓冲区的大小,直接内存DirectMemory的大小默认为 -Xmx 的JVM堆的最大值,但是并不受其限制,而是由JVM参数 MaxDirectMemorySize单独控制

实例验证1:MaxDirectMemorySize

    public static void main(String[] args) {
        ByteBuffer.allocateDirect(100 * 1024 * 1024); // 100MB
    }

运行之后,程序正常结束。

然后设置 -Xmx100M,程序运行失败,抛出 OutOfMemoryError异常。

然后再配置: -Xmx100M  -XX:MaxDirectMemorySize=200M,程序正常结束。

run -> edit configuration 中修改 vm option

    

实例验证2:直接缓冲区的直接内存不是分配在JVM堆中

    public static void main(String[] args) {
        for (int i = 0; i < 20000; i++) {
            ByteBuffer.allocateDirect(1024 * 100);  //100K
//            ByteBuffer.allocate(1024 * 100);  //100K 不会触发full gc
        }
    }

  先运行程序之前设置 JVM参数 -XX:+PrintGC, 

    

我们看到这里执行 GC的次数较少,但是触发了 Full GC,原因在于直接内存不受 GC(新生代的Minor GC)影响,

只有当执行老年代的 Full GC时候才会顺便回收直接内存!而直接内存是通过存储在JVM堆中的DirectByteBuffer对象来引用的,

所以当众多的DirectByteBuffer对象从新生代被送入老年代后才触发了 full gc。

实验验证3:文件复制,操作时间对比

    public static void main(String[] args) {
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            long start = System.currentTimeMillis();
            // 1.创建FileChannel
//            inputStream = new FileInputStream("D:/E/NIO/text.txt");
//            outputStream = new FileOutputStream("D:/E/NIO/text_copy.txt");
//            inChannel = inputStream.getChannel();
//            outChannel = outputStream.getChannel();

            inputStream = new FileInputStream("D:/E/NIO/textBig.txt");
            outputStream = new FileOutputStream("D:/E/NIO/textBig_copy.txt");
            inChannel = inputStream.getChannel();
            outChannel = outputStream.getChannel();

            // 2.缓冲区
//            ByteBuffer buffer = ByteBuffer.allocate(1024);
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

            // 3.1 FileChannel读取数据,将通道中的读取数据到写入缓冲区中
            int readLength = inChannel.read(buffer);
            while (readLength != -1){
                buffer.flip();
                while (buffer.remaining() > 0){
                    // 3.2 FileChannel写入数据,将缓冲区中的数据写入到通道中
                    outChannel.write(buffer);
                }
                // 清空当前的缓冲区的数据,缓冲区重复使用
                buffer.clear();
                readLength = inChannel.read(buffer);
            }

            long end = System.currentTimeMillis();
            System.out.println("所需时间为:" + (end - start));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭资源
            if (inChannel != null) {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outChannel != null) {
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

间接缓冲区操作时间:35M--1131ms,1.56G--47686ms

直接缓冲区操作时间:35M--726ms,  1.56G--25163ms

结论:

直接缓冲区比间接缓冲区操作时间要快;

直接缓冲区中,1024即一次性分配整个文件长度大小的堆外内存,越大操作时间越小。但是注意:DirectMemory的内存只有在 JVM执行 full gc 的时候才会被回收,那么如果在其上分配过大的内存空间,可能会出现 OutofMemoryError异常,慎用。

通过 MappedByteBuffer操作大文件的方式,其读写性能极高!推荐使用MappedByteBuffer。

 

参考文章:有些参考文章链接搞丢了,图来自网络,

BIO、NIO和AIO的区别

以Java的视角来聊聊BIO、NIO与AIO的区别?

 

—— Stay Hungry. Stay Foolish. 求知若饥,虚心若愚。

你可能感兴趣的:(Java,Channel,Buffer,Selector)