您当前的位置: 首页 >  Java

Charge8

暂无认证

  • 2浏览

    0关注

    447博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

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

Charge8 发布时间:2020-07-26 20:38:37 ,浏览量:2

一、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  voidforce(boolean metaData)           强制将所有对此通道的文件更新写入包含该文件的存储设备中。 FileLocklock()           获取对此通道的文件的独占锁定。abstract  FileLocklock(long position, long size, boolean shared)           获取此通道的文件给定区域上的锁定。abstract  MappedByteBuffermap(FileChannel.MapMode mode, long position, long size)           将此通道的文件区域直接映射到内存中。abstract  longposition()           返回此通道的文件位置。abstract  FileChannelposition(long newPosition)           设置此通道的文件位置。abstract  intread(ByteBuffer dst)           将字节序列从此通道读入给定的缓冲区。 longread(ByteBuffer[] dsts)           将字节序列从此通道读入给定的缓冲区。abstract  longread(ByteBuffer[] dsts, int offset, int length)           将字节序列从此通道读入给定缓冲区的子序列中。abstract  intread(ByteBuffer dst, long position)           从给定的文件位置开始,从此通道读取字节序列,并写入给定的缓冲区。abstract  longsize()           返回此通道的文件的当前大小。abstract  longtransferFrom(ReadableByteChannel src, long position, long count)           将字节从给定的可读取字节通道传输到此通道的文件中。abstract  longtransferTo(long position, long count, WritableByteChannel target)           将字节从此通道的文件传输到给定的可写入字节通道。abstract  FileChanneltruncate(long size)           将此通道的文件截取为给定大小。 FileLocktryLock()           试图获取对此通道的文件的独占锁定。abstract  FileLocktryLock(long position, long size, boolean shared)           试图获取对此通道的文件给定区域的锁定。abstract  intwrite(ByteBuffer src)           将字节序列从给定的缓冲区写入此通道。 longwrite(ByteBuffer[] srcs)           将字节序列从给定的缓冲区写入此通道。abstract  longwrite(ByteBuffer[] srcs, int offset, int length)           将字节序列从给定缓冲区的子序列写入此通道。abstract  intwrite(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)           创建从中读取和向其中写入(可选)的随机访问文件流,该文件具有指定名称。 FileChannelgetChannel()           返回与此文件关联的唯一 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 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. 求知若饥,虚心若愚。

关注
打赏
1664721914
查看更多评论
立即登录/注册

微信扫码登录

0.0399s