Java BIO、NIO、AIO编程-总结
发布日期:2021-05-07 23:39:08 浏览次数:25 分类:精选文章

本文共 18377 字,大约阅读时间需要 61 分钟。

Java BIO、NIO、AIO编程


如想了解更多更全面的Java必备内容可以阅读:所有JAVA必备知识点面试题文章目录:


来…直接进入主题:

文章目录


1、同步与异步,阻塞与非阻塞的区别是什么?

  • 同步(Synchronous): 指发送一个请求,需要等待执响应后,然后才能够发送下一个请求,有个等待过程,需要所有都执行完才会返回结果,请求时间过长给人一种卡死的状态。
  • 异步(Asynchronous): 指发送一个请求,不需要等待响应,随时可以发送下一次请求。
  • 阻塞(blocking): A调用B,A被挂起直到B返回结果给A,才能继续执行。调用结果返回前,当前线程挂起不能够处理其他任务,一直等待调用结果返回。
  • 非阻塞(no-blocking): A调用B,A不会被挂起,A可以执行其他操作。调用结果返回前,当前线程不挂起,可以处理其他任务。

2、什么是IO?

I/O :指是以流为基础进行数据的输入输出的,所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。

3、什么是BIO?

同步阻塞IO(blocking IO):是JDK 1.0传统的 java.io 包,它是基于IO流模型实现的,交互的方式是同步阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。缺点就是 IO 的效率和扩展性很低,高并发量时容易成为应用性能瓶颈。

4、什么伪异步 IO?

伪异步I/O 通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层任然是同步阻塞的BIO模型,因此无法从根本上解决问题。

5、什么是NIO?

同步非阻塞IO(no-blocking IO):是JDK 1.4 引入的 java.nio 包,提供了 通道(Channel)选择器(Selector)缓冲区(Buffer) 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。

6、什么是AIO?

异步非阻塞IO(Asynchronous IO):是JDK 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,异步 IO 是基于事件和回调机制实现的。采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。

7、聊聊BIO、NIO和AIO的区别?

BIO NIO AIO
JDK 1.0引入的java.io 包 JDK 1.4引入的java.nio 包 JDK 1.7引入的java.nio 包
阻塞的 非阻塞 非阻塞
同步 同步 异步
面向流 NIO是面向缓冲区 AIO是面向缓冲区,基于事件和回调机制
一个连接一个线程 一个请求一个线程 一个有效请求一个线程
可靠性差 可靠性好 可靠性好
吞吐量低 吞吐量高 吞吐量高

8、你知道IO流的分类都有哪些?

8.1 按照读写的单位大小来分,分成哪些流?
  • 字符流: 以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。(Java代码接收数据为一般为char数组,也可以是别的)
  • 字节流: 以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。(Java代码接收数据只能为byte数组)
8.2 按照实际IO操作来分,分成哪些流?

输入输出都是相对于 “内存” 而言

  • 输出流: 就是将数据从内存写入到各种输入设备(包括文件、键盘等)中。所有输出流都是抽象类OutputStream(字节输出流)或者Writer(字符输出流)的子类。对于内存来说是:将数据(内存中) “写(writer)” 入到外界设备中。
  • 输入流: 就是将数据从各种输入设备(包括文件、键盘等)中读取到内存中。所有输入流类都是抽象类InputStream(字节输入流),或者抽象类Reader(字符输入流)的子类。对于内存来说是:将外界设备中的数据 “读(read)” 入内存中。
8.3 按照读写时是否直接与硬盘,内存等节点连接分,分成哪些流?
  • 节点流: 直接与数据源相连,读入或读出。
  • 处理流: 也叫包装流,是对一个对于已存在的流的连接进行封装,通过所封装的流的功能调用实现数据读写。如添加个Buffer缓冲区。注意:为什么要有处理流?主要作用是在读入或写出时,对数据进行缓存,以减少I/O的次数,以便下次更好更快的读写文件,才有了处理流。

在这里插入图片描述

简单列举如下java.io部分操作类:
在这里插入图片描述

9、你知道什么是内核空间吗?

我们的应用程序是不能直接访问硬盘的,我们程序没有权限直接访问,但是操作系统(Windows、Linux…)会给我们一部分权限较高的内存空间,它叫内核空间。

在这里插入图片描述

10、说说五种IO模型分别是什么?

同步阻塞BIO(blocking I/O)、同步非阻塞NIO(no-blocking I/O)、异步非阻塞AIO(asynchronous I/O)、信号驱动IO(signal blocking I/O)、IO多路转接(I/O multiplexing)。

11、说说什么叫对象序列化,什么是反序列化?

  • 对象序列化:将对象以二进制的形式保存在硬盘上。
  • 反序列化:将二进制的文件转化为对象读取。

12、实现序列化接口是时候一般要生成一个serialVersionUID字段,怎么生成SerialversionUID?一般有什么用?

可序列化类可以通过声明名为 “serialVersionUID” 的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)。如果用户没有自己声明一个serialVersionUID,接口会默认生成一个serialVersionUID。

强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致InvalidClassException这个异常。

比如说先进行序列化,然后在反序列化之前修改了类,那么就会报错。因为修改了类,对应的SerialversionUID也变化了,而序列化和反序列化就是通过对比其SerialversionUID来进行的,一旦SerialversionUID不匹配,反序列化就无法成功。

13、你知道为什么图片、视频、音乐、文件等 一般要用字节流来读取吗?

这个很基础,你看看你电脑文件的属性就好了,CPU规定了计算机存储文件都是按字节算的。

在这里插入图片描述

14、说出你对 IO 的最佳实践?

IO 对 Java 应用的性能非常重要。理想情况下,你不应该在你应用的关键路径上 避免 IO 操作。下面是一些你应该遵循的 Java IO 最佳实践:

a)尽量使用有缓冲区的 IO 类,而不要单独读取字节或字符。
b)使用 NIO 和 NIO2
c)在 finally 块中关闭流,或者使用 try-with-resource 语句。
d)使用内存映射文件获取更快的 IO。

15、你知道NIO 由哪些核心组件组成吗?

核心组件由缓存区(Buffer)通道(channel)选择器(selector) 组成。

在这里插入图片描述
缓存区(Buffer)通道(channel)选择器(selector) 的关系入下图所示:
注意:buffer和XXXChannel都是双向的,既可以读也可以写,所以下图buffer和Channel的箭头流向是不合适。
在这里插入图片描述

16、对 NIO 通道(Channel)/ 缓冲区(Buffer)的了解多少?

16.1 什么是缓冲区(Buffer)?

缓冲区(Buffer) 一个用于特定基本数据类型的容器。由java.nio 包定义的,所有缓冲区都是Buffer 抽象类的子类。主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。

Buffer就像一个数组(底层完全是数组实现的 例:IntBuffer --> final int[] hb;),可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下Buffer常用子类:
在这里插入图片描述
MappedByteBuffer:可以让文件直接在内存(堆外内存)修改,操作系统不需要做拷贝,即:直接缓冲区。

16.2 缓冲区有哪几个重要属性?

Buffer jdk1.6实现源码:

public abstract class Buffer {       // Invariants: mark <= position <= limit <= capacity    private int mark = -1;    private int position = 0;    private int limit;    private int capacity;	........

遵循规则:0 <= mark<= position <= limit<= capacity

  • capacity : 缓冲区的容量,是它所包含的元素的数量。在缓冲区创建时被设定,不能为负并且不能更改。
  • position :当前操作缓冲区的位置,是下一个要读取或写入的元素的索引。不能为负,并且不能大于 limit。
  • limit : 缓冲区的界限,第一个不应该读取或写入的数据的索引,即位于limit后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
  • mark:标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。

例如:经过 allocate(10)–> put()–>flip()操作之后,capacity和position和limit的变化如下:

在这里插入图片描述
flip() jdk1.6底层源码:

public final Buffer flip() {   	limit = position;	position = 0;	mark = -1;	return this;    }
16.3 ByteBuffer类提供了4个静态工厂方法,来获得ByteBuffer的实例,分别是什么?
方法 方法描述
allocate(int capacity) 从堆空间中分配一个容量大小为capacity的byte数组作为缓冲区的byte数据存储器
allocateDirect(int capacity) 是不使用JVM堆栈而是通过操作系统来创建内存块用作缓冲区,它与当前操作系统能够更好的耦合,因此能进一步提高I/O操作速度。但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并长期存在,或者需要经常重用时,才使用这种缓冲区
wrap(byte[] array) 这个缓冲区的数据会存放在byte数组中,bytes数组或buff缓冲区任何一方中数据的改动都会影响另一方。其实ByteBuffer底层本来就有一个bytes数组负责来保存buffer缓冲区中的数据,通过allocate方法系统会帮你构造一个byte数组
wrap(byte[] array, int offset, int length) 在上一个方法的基础上可以指定偏移量和长度,这个offset也就是包装后byteBuffer的position,而length呢就是limit-position的大小,从而我们可以得到limit的位置为length+position(offset)
16.4 列举NIO一些常用的方法?
方法 方法描述
limit(), limit(10)等 其中读取和设置这4个属性的方法的命名和jQuery中的val(),val(10)类似,limit()负责get,limit(10)负责set
reset() 把position设置成mark的值,相当于之前做过一个标记,现在要退回到之前标记的地方
clear() position = 0;limit = capacity;mark = -1; 有点初始化的味道,但是并不影响底层byte数组的内容
flip() limit = position;position = 0;mark = -1; 反转读写模式,也就是让flip之后的position到limit这块区域变成之前的0到position这块,翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态
rewind() 把position设为0,mark设为-1,不改变limit的值
remaining() return limit - position;返回limit和position之间相对位置差
hasRemaining() return position < limit返回是否还有未读内容
compact() 把从position到limit中的内容移到0到limit-position的区域内,position和limit的取值也分别变成limit-position、capacity。如果先将positon设置到limit,再compact,那么相当于clear()
get() 相对读,从position位置读取一个byte,并将position+1,为下次读写作准备
get(int index) 绝对读,读取byteBuffer底层的bytes中下标为index的byte,不改变position
get(byte[] dst, int offset, int length) 从position位置开始相对读,读length个byte,并写入dst下标从offset到offset+length的区域
put(byte b) 相对写,向position的位置写入一个byte,并将postion+1,为下次读写作准备
put(int index, byte b) 绝对写,向byteBuffer底层的bytes中下标为index的位置插入byte b,不改变position
put(ByteBuffer src) 用相对写,把src中可读的部分(也就是position到limit)写入此byteBuffer
put(byte[] src, int offset, int length) 从src数组中的offset到offset+length区域读取数据并使用相对写写入此byteBuffer
16.5 Buffer 读取和写入数据通常遵循哪个四个步骤?
  1. 将数据写入缓冲区
  2. 调用 buffer.flip() 反转读写模式(将缓冲区从写入模式切换到读取模式)
  3. 从缓冲区读取数据
  4. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容(区别在于 clear() 是方法清除整个缓冲区,而 compact() 方法仅清除已读取的数据,未读数据都会移动到缓冲区的开头,新数据将在未读数据之后写入缓冲区。)
16.6 缓冲区是怎么分配的?

要获取 Buffer 对象,必须先分配它。 比如每个 Buffer 类都有一个 allocate() 方法来执行此操作。 下面是一个显示ByteBuffer分配的示例,容量为1024字节:

ByteBuffer buffer = ByteBuffer.allocate(1024); //创建容量为1024字节的缓冲区
16.7 怎么将数据写入缓冲区?

可以通过两种方式将数据写入 Buffer:

  1. 将数据从通道写入缓冲区
  2. 通过缓冲区的 put() 方法,自己将数据写入缓冲区。
16.8 说说flip()方法是什么意思?

flip() 方法将 Buffer 从写入模式切换到读取模式。 调用 flip() 会将 position 设置回 0,并将 limit 的值设置为切换之前的 position 值。换句话说,limit 表示之前写进了多少个 byte、char 等 —— 现在能读取多少个 byte、char 等。

// flip() jdk1.6源码public final Buffer flip() {   	limit = position;	position = 0;	mark = -1;	return this;    }
16.9 怎么从缓冲区读取数据?

有两种方法可以从 Buffer 中读取数据:

  1. 将数据从缓冲区读入通道。
  2. 使用 get() 方法之一,自己从缓冲区读取数据。
16.10 说说rewind() 方法有什么作用?

Buffer对象的 rewind() 方法将 position 设置回 0,因此可以重读缓冲区中的所有数据, limit 则保持不变。

//rewind() jdk1.6源码    public final Buffer rewind() {   	position = 0;	mark = -1;	return this;    }
16.11 你知道clear() 方法和 compact()方法有什么区别吗?

clear(): 如果调用 clear() ,则将 position 设置回 0 ,并将 limit 被设置成 capacity 的值。换句话说,Buffer 被清空了。 但是 Buffer 中的实际存放的数据并未清除。如果在调用 clear() 时缓冲区中有任何未读数据,数据将被“遗忘”,这意味着不再有任何标记告诉读取了哪些数据,还没有读取哪些数据。

//clear() jdk1.6源码    public final Buffer clear() {   	position = 0;	limit = capacity;	mark = -1;	return this;    }

compact():如果缓冲区中仍有未读数据,并且想稍后读取它,但需要先写入一些数据,这时候应该调用 compact() ,它会将所有未读数据复制到 Buffer 的开头,然后它将 position 设置在最后一个未读元素之后。 limit 属性仍设置为 capacity ,就像 clear() 一样。 现在缓冲区已准备好写入,并且不会覆盖未读数据。

16.12 说说mark() 方法和 reset()方法分别的作用?

以通过调用 Buffer 对象的 mark() 方法在 Buffer 中标记给定位置。 然后,可以通过调用 Buffer.reset() 方法将位置重置回标记位置。

buffer.mark();// 调用 buffer.get() 等方法读取数据.........buffer.reset();  // 设置 position 回到 mark 位置。
//mark() jdk1.6源码    public final Buffer mark() {   	mark = position;	return this;    }	//reset() jdk1.6源码    public final Buffer reset() {           int m = mark;	if (m < 0)	    throw new InvalidMarkException();	position = m;	return this;    }
16.13 什么是通道(Channel)?

通道(Channel):NIO 中的所有 IO 都以 Channel 开头,通道有点像流。 数据可以从 Channel 读入 Buffer,也可以从 Buffer 写入 Channel 。Channel 负责双向传输, Buffer 负责存储。

NIO 通道类似于流,但有一些区别:

  • 通道可以读取和写入。 流通常是单向的(读或写)。
  • 通道可以异步读取和写入。
  • 通道始终读取或写入缓冲区,即它只面向缓冲区。
    在这里插入图片描述
16.14 说说 NIO 中主要的 Channel 实现类有哪些?
  • FileChannel :文件通道,主要用来对本地文件进行IO操作

    • public abstract int read(ByteBuffer dst) :从通道读取数据并放到缓冲区中,【读/写 针对的是通道Channel】
    • public abstract int write(ByteBuffer src) :把缓冲区数据写到通道中,【读/写 针对的是通道Channel】
    • public abstract long transferFrom(ReadableByteChannel src,long position, long count) :把src(目标通道)数据复制到当前通道。
    • public abstract long transferTo(long position, long count,WritableByteChannel target) :把数据从当前通道复制给target(目标通道),拷贝速度很快。
      底层实现了零拷贝(零拷贝:操作系统CPU拷贝,而不是 DMA拷贝 )。
      在linux下一个transferTo可以实现整个文件的拷贝;
      在windows下transferTo一次调用只能发送8M的数据,就需要分段传输
    • ……
  • DatagramChannel :是可以发送和接收 UDP 数据包的通道

  • SocketChannel :连接到 TCP 网络套接字的通道(客户端),主要负责读写操作,将缓冲区数据写入通道,或者将通道数据读取到缓冲区。

    • jdk1.7 实现 implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel。
    • public static SocketChannel open():得到一个SocketChannel
    • public abstract SocketChannel bind(SocketAddress local):绑定服务器。
    • public abstract boolean connect(SocketAddress remote):连接服务器。
    • public abstract boolean finishConnect():如果connect方法连接失败,接下来通过该方法完成连接操作。
    • public abstract int read(ByteBuffer dst):从通道读数据
    • public abstract int write(ByteBuffer src):往通道写数据
    • ……
  • ServerSocketChannel :用于监听TCP连接的通道(服务器端)

    • public static ServerSocketChannel open():得到一个ServerSocketChannel
    • public final ServerSocketChannel bind(SocketAddress local):设置服务器端口
    • public abstract SelectionKey register(Selector sel, int ops, Object att):将当前通道注册到sel(选择器)中,并设置监听的事件。
    • ……
16.15 (Socket,ServerSocket)与(SocketChannel,ServerSocketChannel)区别和联系?
  • Socket 和ServerSocke 是一对,它们是java.net下面实现socket通信的类。
  • SocketChannel 和ServerSocketChannel是一对,它们是java.nio下面实现通信的类 支持异步通信。
  • 服务器必须先建立ServerSocket或者ServerSocketChannel,来等待客户端的连接。
  • 客户端必须建立相对应的Socket或者SocketChannel,与服务器建立连接。
16.16 什么Scatter / Gather 通道的聚集和分散?

NIO 具有内置的 Scatter/Gather 支持,用于描述读取和写入通道的操作。

  • 分散(Scatter):从 Channel 中将数据读入多个 Buffer 的操作。
  • 聚集(Gather):是将多个Buffer 的数据写入单个Channel 的操作。
16.17 什么是分散读取(scattering Reads)和聚集写入(gathering Writes)?

分散读取(scattering Reads):将单个通道中的数据分散到多个缓冲区中。

注意:多个缓冲区首先插入到数组中,然后将数组作为参数传递给 channel.read() 方法。 然后,read()方法按照缓冲区在数组中出现的顺序从通道写入数据。 一旦缓冲区已满,通道就会继续填充下一个缓冲区。
在这里插入图片描述

聚集写入(gathering Writes):将多个缓冲区的数据聚集到单个通道中。

将缓冲区数组传递给 write() 方法,该方法按照在数组的顺序写入缓冲区的内容到单个通道,注意仅写入缓冲区的 position 和 limit 之间的数据。 因此,与 Scattering Reads 相比,Gathering Writes 可以适应大小不固定的数据,因为它只把包含内容部分的缓冲区写入到通道。
在这里插入图片描述

17、对 NIO 选择器(Selector)了解多少?

17.1 什么是选择器(Selector)?

选择器(Selector) 是一个 NIO 组件,它可以检测一个或多个 NIO 通道,并确定哪些通道可以用于读或写了。Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector上)。

一个选择器可对应多个通道,选择器是通过 SelectionKey 这个关键对象完成对多个通道的选择的。注册选择器的时候会返回此对象,调用选择器的 selectedKeys() 方法也会返回此对象。每一个 SelectionKey 都包含了一些必要信息,比如关联的通道和选择器,获取到 SelectionKey 后就可以从中取出对应通道进行操作。

1 个 Selector 处理 3 个 Channel 的线程图示:

在这里插入图片描述

17.2 Selector、SelectionKey、ServerSocketChannel和SocketChannel关系?
  1. 当客户端连接时,会通过ServerSocketChannel得到SocketChannel。
  2. 将SocketChannel注册到Selector上,通过:register(Selector sel,int ops),一个Selector上可以注册多个SocketChannel。
  3. 注册后会返回一个SelectionKey,会和该Selector相关联(集合)
  4. Selector进行监听,使用select方法,返回有事件发生的通道对应的SelectionKey。
  5. 通过SelectionKey反向获取SocketChannel,通过channel()方法。
  6. 通过得到的channel可以进行相应的操作,完成业务处理。
    在这里插入图片描述
17.3 怎样创建一个选择器?
// 打开选择器Selector selector = Selector.open();
17.4 怎么设置通道不阻塞?选择器怎么注册通道?

要使用带选择器的通道,必须使用选择器来注册通道。 这是使用关联 Channel 对象的 register() 方法完成的,如下所示:

//不阻塞----通道必须处于非阻塞模式才能与选择器一起使用。这意味着无法将 FileChannel 与 Selector一 起使用channel.configureBlocking(false);//使用通道注册一个选择器SelectionKey key = channel.register(selector, SelectionKey.OP_READ);while(true) {       int readyChannels = selector.select();    if(readyChannels == 0){        	continue;    }    // 这里的 SelectionKey 就和注册时候返回的 key 一样,    // 因为一个选择器可以注册多个通道,所以这里返回集合    Set
selectedKeys = selector.selectedKeys(); Iterator
keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // ServerSocketChannel接受了一个连接。 } else if (key.isConnectable()) { // 与远程服务器建立连接。 } else if (key.isReadable()) { // 一个通道已读就绪 } else if (key.isWritable()) { // 一个通道已写就绪 } keyIterator.remove(); }}
17.5 SelectionKey枚举有哪些,分别都是什么意思?

注意如上代码中, register() 方法的第二个参数。 这是一个“ interest 集合”,意味着通过 Selector 在 Channel 中监听哪些事件。可以收听四种不同的事件:

  • SelectionKey.OP_READ:读, 读操作,值为:1。
    源码: public static final int OP_READ = 1 << 0;
  • SelectionKey.OP_WRITE:写,写操作,值为:4。
    源码: public static final int OP_WRITE = 1 << 2;
  • SelectionKey.OP_CONNECT:连接,连接已经建立,值为:8。
    源码: public static final int OP_CONNECT = 1 << 3;
  • SelectionKey.OP_ACCEPT:接收,有新的网络(客户端)进行连接,值为:16。
    源码: public static final int OP_ACCEPT = 1 << 4;

如果要监听多个事件,那么可以用“|”位或操作符将常量连接起来。

17.6 SelectionKey有哪些常用的方法?
方法源码 描述
public abstract Selector selector() 得到与之关联的Selector对象
public abstract SelectableChannel channel() 得到与之关联的通道
public final Object attachment() 得到与之关联的共享数据(缓冲区数据)
public abstract SelectionKey interestOps(int ops) 设置或改变监听事件
public final boolean isReadable() 是否可以读
public final boolean isWritable() 是否可以写
public final boolean isAcceptable() 是否可以建立连接
public final boolean isConnectable() 是否已连接
17.7 说说register() 方法返回的 SelectionKey 对象都包含哪些主要属性?

正如在上一节中看到的,当使用 Selector 注册 Channel 时,register() 方法返回一个 SelectionKey 对象。 这个 SelectionKey 对象包含一些有趣的属性:

  • interest 集合:interest 集合是所选择的感兴趣的事件集合,可以通过 SelectionKey 读取和写入 Interest 集合,如下所示:

    //可以使用给定的 SelectionKey 常量和 interest 集合进行“&”位与操作,以查明某个事件是否在 interest 集合中。int interestSet = selectionKey.interestOps();boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
  • ready 集合:就绪集是通道准备好的一组操作。 将在 Selector 后访问就绪集。

  • 对应 Channel:SelectionKey 访问通道

    Channel  channel  = selectionKey.channel();
  • 对应 Selector:SelectionKey 访问选择器

    Selector selector = selectionKey.selector();
  • 附加对象(可选):可以将对象或者更多信息附加到 SelectionKey。

17.8 怎么通过选择器选择通道?

使用 Selector 注册一个或多个通道后,可以调用其中一个 select() 方法。

以下是 select() 方法,返回的 int 表示有多少通道准备好了:

  • selector.select() : 将一直阻塞,直到至少有一个频道为注册的事件做好准备。
  • selector.select(1000) :与 select() 相同,但它会最长阻塞 1000毫秒。
  • selector.wakeup():唤醒selector
  • selector.selectNow() :完全没有阻塞。 它会立即返回任何已准备好的通道。
17.9 说说wakeUp()方法的作用?

已调用 select() 方法的线程可能会被阻塞,这是可以通过调用 wakeUp() 方法离开 select() 方法,即使尚未准备好任何通道。其它线程来调用阻塞线程 Selector 对象的 select() 即可让阻塞在 select() 方法上的线程立马返回。

如果另一个线程调用 wakeup() 并且当前在 select() 中没有阻塞线程,则调用 select() 的下一个线程将立即被“唤醒”。

17.10 说说close() 方法的作用?

调用选择器的 close() 方法将关闭 Selector 并使使用此 Selector 注册的所有 SelectionKey 实例失效。 但通道本身并不会被关闭。

18、直接缓冲区与非直接缓冲区的区别?

  • 非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中。

    在JVM中内存中创建,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复制操作。
    应用程序读取数据过程:物理磁盘----读---->物理内存----copy---->JVM内存----读---->应用程序
    应用程序写入数据过程:应用程序----写---->JVM内存----copy---->物理内存----写---->物理磁盘在这里插入图片描述

  • 直接缓冲区:通过调用此类的 allocateDirect() 工厂方法来创建,将缓冲区建立在物理内存中。可以提高效率。

    在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容),少了复制的过程
    直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。
    应用程序读取数据过程:物理磁盘----读---->物理内存映射文件----读---->应用程序
    应用程序写入数据过程:应用程序----写---->物理内存映射文件*----写---->物理磁盘
    在这里插入图片描述

19、聊聊对JAVA AIO框架的了解?

异步IO则采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。

异步IO对操作系统进行的支持:

  • 微软的windows系统提供了一种异步IO技术:IOCP(I/O CompletionPort,I/O完成端口)
  • Linux下由于没有这种异步IO技术,所以使用的是 epoll 对异步IO进行模拟。
    在这里插入图片描述

20、与相比NIO,AIO对应哪些抽象通道类?

  • AsynchronousFileChannel :异步文件通道
  • AsynchronousDatagramChannel :是可以发送和接收 UDP 数据包的异步通道 【在jdk1.7中已经被移除了】
  • AsynchronousSocketChannel :连接到 TCP 网络套接字的异步通道(客户端)
  • AsynchronousServerSocketChannel :用于监听TCP连接的异步通道(服务器端)

21、列举一下AIO有哪些重要的类?

  1. AsynchronousServerSocketChannel:服务端Socket通道类,负责服务端Socket的创建和监听;是ServerSocketChannel的异步版本的通道,支持异步处理。AsynchronousServerSocketChannel的使用和ServerSocketChannel一样需要经过三个步骤:

    • 创建/打开通道
      AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
    • 绑定地址和端口
      //构建一个InetSocketAddress实例以指定监听的地址和端口,如果需要指定ip,则调用InetSocketAddress(ip,port)构造方法创建即可serverSocketChannel.bind(new InetSocketAddress(port));
    • 监听客户端连接请求
      public abstract  void accept(A,CompletionHandler
      );public abstract Future
      accept();
  2. AsynchronousSocketChannel:客户端Socket通道类,负责客户端消息读写;客户端可以通过调用AsynchronousSocketChannel静态方法open()创建,而服务端则通过调用AsynchronousServerSocketChannel.accept()方法后由AIO内部在合适的时候创建。

    • 创建AsynchronousSocketChannel
      // 打开一个socket通道AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();// 阻塞等待连接成功socketChannel.connect(new InetSocketAddress(ip,port)).get();// 连接成功,接下来可以进行read、write操作
    • connect:也提供了CompletionHandler回调和Future返回值两个重载方法
      // 基于回调public abstract  void connect(SocketAddress remote, A attachment, CompletionHandler
      handler);// 基于Future 调用get()方法阻塞等待连接建立完成public abstract Future
      connect(SocketAddress remote);
    • 发送消息:可以构建一个ByteBuffer对象并调用socketChannel.write(ByteBuffer)方法异步发送消息,并通过CompletionHandler回调接收处理发送结果
      ByteBuffer writeBuf = ByteBuffer.wrap("From socketChannel:Hello i am socketChannel".getBytes());socketChannel.write(writeBuf, null, new CompletionHandler
      () {   @Override  public void completed(final Integer result, final Object attachment) {     // 发送完成,result:总共写入的字节数  }  @Override  public void failed(final Throwable exc, final Object attachment) {     // 发送失败  }});
    • 读取消息:构建一个指定接收长度的ByteBuffer用于接收数据,调用socketChannel.read()方法读取消息并通过CompletionHandler处理读取结果
      ByteBuffer readBuffer = ByteBuffer.allocate(128);socketChannel.read(readBuffer, null, new CompletionHandler
      () {   @Override  public void completed(final Integer result, final Object attachment) {     // 读取完成,result:实际读取的字节数。如果通道中没有数据可读则result=-1。  }  @Override  public void failed(final Throwable exc, final Object attachment) {     // 读取失败  }});
  3. AsynchronousChannelGroup:异步channel的分组管理,目的是为了资源共享。

    一个AsynchronousChannelGroup绑定一个线程池,这个线程池执行两个任务:处理IO事件和派发CompletionHandler。
    通过AsynchronousServerSocketChannel创建的 AsynchronousSocketChannel将同属于一个组,共享资源。

  4. CompletionHandler:消息处理回调接口,是一个负责消费异步IO操作结果的消息处理器。

    AIO中大部分的异步I/O操作接口都封装了一个带CompletionHandler类型参数的重载方法,使用CompletionHandler可以很方便地处理AIO中的异步I/O操作结果。

后续更多关于BIO、NIO、AIO编程的相关内容会不断更新中……

······

帮助他人,快乐自己,最后,感谢您的阅读!
所以如有纰漏或者建议,还请读者朋友们在评论区不吝指出!

…知识是一种宝贵的资源和财富,益发掘,更益分享…

上一篇:浅析Java线程池实现原理及在实际业务中的实践
下一篇:Java Socket网络编程-总结

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2025年03月29日 04时58分31秒