NIO 的工作方式

BIO 带来的挑战

BIO : BIO 通信模型,通常由一个独立的 Acceptor 线程负责监听客户端的连接,接受到请求之后,为每个客户端创建一个新的线程进行链路处理,处理完成之后,线程销毁。是典型的 请求-应答通信模型。

BIO 即阻塞 IO,不管是磁盘IO 还是 网络 IO,数据在写入 OutputStream 或者从 InputStream 读取时都有可能会阻塞,一旦有阻塞,线程将会失去 CPU 的使用权,这在当前的大规模访问量和有性能要求的情况下是不能被接受的。

虽然当前的网络IO 有一些解决方法,如一个客户端对应一个处理线程,出现阻塞时只是一个线程阻塞而不会影响到其他线程的工作;还有问了减少系统线程开销,使用线程池的办法减少线程创建和回收成本,但是在一些场景下仍然无法解决。

如:当前一些需要大量 HTTP 长连接的情况,像淘宝现在使用的 web 旺旺,服务端需要同时保持几百万的 HTTP 连接,但并不是每时每刻这些连接都需要传输数据,在这种情况下不可能同时创建这么多线程来保持连接.

即使线程数量不是问题,也仍然会有一些问题无法避免:比如我们想给某些客户端更高的服务优先级,很难通过设计线程的优先级来完成。

另外一种情况是,每个客户端的请求在服务端可能需要访问一些竞争资源,这些客户端在不同的线程中,因此需要同步,要实现这种同步操作远比单线程复杂得多。以上这些情况都说明,我们需要一种新的 IO 操作方式。

NIO 的工作机制

首先看下 NIO 相关的类图:



图中有两个关键的类:Selector 和 Channel,他们是 NIO 中两个核心概念。

我们使用城市交通工具来描述 NIO 的工作方式。这里的 Channel 比 Socket 更加具体,可以比喻为某种具体的交通工具,如汽车,而把 Selector 理解为车辆运行调度系统,它负责监控每辆车的运行状态,是未出站还是在路上等。也就是 Selector 可以轮询每个 Channel 的状态。这里还有一个 Buffer 类,它比 Stream 更加具体的概念,Stream 代表座位,但没有描述座位是什么,以及是否还有座位。而 NIO 引入了 Channel、Selector 、Buffer 就是想把这些信息具体化,让程序员有机会去控制他们。

例如:当我们调用 write() 方法向 sendQ 中写入数据时,当一次写入的数据超过了 SendQ 的长度时,需要按照 SendQ 的长度进行分割,在这个过程中需要将用户地址和内核地址空间进行切换,而这个切换是你不能控制的,但在 Buffer 中我们可以自定义 Buffer 容量、是否扩容以及如何扩容等。

下面 show me the code,看下实际是怎么工作的:

public void selector() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);//设置为非阻塞方式
ssc.socket().bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);// 注册监听的事件
while(true){
Set selectedKeys = selector.selectedKeys();// 获取所有 key 集合
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel accept = channel.accept();
accept.configureBlocking(false);
accept.register(selector, SelectionKey.OP_READ);
it.remove();
} else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
SocketChannel channel = (SocketChannel) key.channel();
while (true) {
buffer.clear();
int n = channel.read(buffer);
if (n <= 0) {
break;
}
buffer.flip();
}
it.remove();
}
}
} }

流程简析如下:调用 Selector 的静态方法创建一个选择器,创建一个服务端的 Channel,绑定到一个 Socket 对象,并把这个通信信道注册到选择器上,然后把通信信道设置为“非阻塞”模式。

然后就可以调用 Selector 的 selectedKeys 方法检查已经注册在这个选择器上的所有通信信道是否有事件发生。

如果有,将会返回所有的 SelectionKey ,通过这个对象的 channel() 方法可以获取对应的通信信道对象,从而读取通信的数据,而这里读取的数据是 buffer,这个 Buffer 就是我们可以控制的缓冲器。

在上面的这段程序中,将 Server 端的 监听连接请求的事件和处理请求的事件放在一个线程中,但是在事件应用中,我们通常会把他们放在两个线程中:一个线程专门负责监听客户端的连接请求,而且是以阻塞方式执行;另外一个线程专门负责处理请求,这个专门负责处理请求的线程才会真正采用 NIO 的方式,像 web 服务器 Tomcat 和 Jetty 都是使用这个处理方式。

Selector 可以同时监听一组通信信道(Channel)上的 IO 状态,前提是这个 Selector 已经注册到这些信道中。Selector 的 select() 方法检查已经注册的通信信道上 IO 是否已经准备好,如果没有至少一个信道 IO 状态有变化,那么 select 方法会阻塞等待或在超时时间后返回0。如果有多个信道有数据,那么将会把这些数据分配到对应的数据 Buffer 中。所以关键的地方是,有一个线程来处理所有连接的数据交互,每个连接的数据交互都不是阻塞方式,所以可同时处理大量的连接请求。

Buffer 的工作方式

前面介绍了 Selector 检测到通信信道 IO 有数据传输时,通过 select 方法取得 SocketChannel,将数据读取或写入 Buffer 缓冲区,下面介绍 Buffer 如何接收和写出数据。

简单理解:把 Buffer 当成是一个基本数据类型的元素列表,它通过几个变量来保存列表中数据的当前位置状态,有4个值--capacity、limit、position、mark

  • capacity :缓冲区数组的长度
  • position:下一个要操作的数据元素位置
  • limit:缓冲区数组中不可操作的下一个元素的位置,limit<=capacity
  • mark:用于记录上一次 position 的位置或者默认为0



1、起始位置:position = 0,capacity=limit=length 数组长度

2、写入3个数据之后,如图3;

3、图3 到 图4 是调用了 flip 方法之后,position 回到起始位置,limit取原position值,此时可以从缓冲区中正确读取这3个数据;

4、在下一次写入数据之前,调用 clear 方法,缓冲区的索引状态回到初始位置;

mark的作用呢?当我们调用 mark 方法时,它会记录当前 position 的前一个位置,当我们调用 reset 时,position 会恢复 mark 记录下来的值。

注意:通过 Channel 获取 IO 数据首先要经过操作系统的 Socket 缓冲区,再将数据复制到 Buffer 中,这个操作系统缓冲区就是底层 TCP 所关联的 RecvQ 或者 SendQ 队列,从操作系统缓冲区到用户缓冲区复制数据比较耗费性能,Buffer 提供了一种直接操作操作系统缓冲区的方式,即 ByteBuffer.allocateDirector(size),这个方法返回的 DirectByteBuffer 就是底层存储空间关联的缓冲区,它通过 Native 代码操作非 JVM 堆的内存空间。每次创建或释放的时候都调用一次 System.gc() 。注意,使用 DirectByteBuffer 可能会引起内存泄漏的问题。在数据量大,生命周期较长的情况下比较合适。

NIO 的数据访问方式

NIO 提供了两种访问文件的优化方法,FileChannel.transferTo/FileChannel.transferFrom;另一个是 FileChannel.map

  • FileChannel.transferXXX

    与传统方式相比,减少了数据从内核到用户空间的复制,数据直接在内核空间中移动,在linux 中使用 sendFile 系统调用。

  • FileChannel.map

    FileChannel.map 将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时,将直接操作这个文件数据,这种方式省去了数据从内核空间向用户空间复制的损耗。这种方式适合对大文件的只读性操作,如文件的MD5校验。炼丹师这种方式是和操作系统的底层 IO 实现相关,如以下代码:

public static void map(String[] args) {
int BUFFER_SIZE = 1024;
String fileName = "test.db";
long fileLength = new File(fileName).length();
int bufferCount = 1 + (int) (fileLength / BUFFER_SIZE);
MappedByteBuffer[] buffers = new MappedByteBuffer[bufferCount];
long remaining = fileLength;
for (int i=0;i<bufferCount;i++) {
RandomAccessFile file;
try {
file = new RandomAccessFile(fileName, "r");
buffers[i] = file.getChannel().map(FileChannel.MapMode.READ_ONLY, i * BUFFER_SIZE, Math.min(remaining, BUFFER_SIZE));
} catch (Exception e) {
e.printStackTrace();
}
remaining -= BUFFER_SIZE;
}
}

NIO 的工作方式的更多相关文章

  1. Java NIO的工作方式

    1.BIO带来的挑战 BIO即阻塞IO,不管是磁盘IO,还是网络IO,数据在写入OutputStream或者从InputStream读取时都有可能发生阻塞,一旦有阻塞,当前线程将会被挂起,即线程进入非 ...

  2. 读书笔记-NIO的工作方式

    读书笔记-NIO的工作方式 1.BIO是阻塞IO,一旦阻塞线程将失去对CPU的使用权,当前的网络IO有一些解决办法:1)一个客户端对应一个处理线程:2)采用线程池.但也会出问题. 2.NIO的关键类C ...

  3. NIO的工作方式

    BIO带来的挑战 BIO 就是我们常说的阻塞I/O , 不论磁盘I/O 还是网络/O ,数据在写入OutputStream 或者从 InutStream 读取数据时都有可能会阻塞,一旦有了阻塞,线程就 ...

  4. Nio经典工作方式

    public void selector() throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); Selector ...

  5. Buffer的工作方式

    1.Buffer的工作方式 前面<java NIO的工作方式>介绍了Selector检测到通信信道I/O有数据传输时,通过select()方法取得SocketChannel,将数据读取或写 ...

  6. dicom通讯的工作方式及dicom标准简介

    本文主要讲述dicom标准及dicom通讯的工作方式.dicom全称医学数字图像与通讯 其实嘛就两个方面 那就是“存储”跟“通讯”. 文件数据组织方式  网络数据组织方式.文件数据组织方式就是解析静态 ...

  7. 通过iMindMap改善你的工作方式的教程

    对于iMindMap 10,已经介绍了很多新增与改进的功能,你以为已经结束了?其实不然,本文,小编还会继续和你分享它的一个新功能与一个更新功能.这两个功能将在不经意间改善你的工作方式. 多媒体支持 在 ...

  8. 输入/输出系统的四种不同工作方式对CPU利用率比较

    程序控制工作方式:输入/输出完全由CPU控制,整个I/O过程中CPU必须等待其完成,因此对CPU的能力限制很大,利用率较低 程序中断工作方式:CPU不再定期查询I/O系统状态,而是当需要I/O处理时再 ...

  9. 从一个简单例子来理解js引用类型指针的工作方式

    <script> var a = {n:1}; var b = a; a.x = a = {n:2}; console.log(a.x);// --> undefined conso ...

随机推荐

  1. 【mq读书笔记】mq事务消息

    关于mq食物以什么样的方式解决了什么样的问题可以参考这里: https://www.jianshu.com/p/cc5c10221aa1 上文中示例基于mq版本较低较新的版本中TransactionL ...

  2. 推荐系统实践 0x0a 冷启动问题

    什么是冷启动问题 如何在没有大量用户数据的情况下设计个性化推荐系统并且让用户对推荐结果满意从而愿意使用推荐系统,就是冷启动问题.冷启动问题主要分为三类: 用户冷启动 物品冷启动 系统冷启动 下面我们将 ...

  3. C#:终于有人把 ValueTask、IValueTaskSource、ManualResetValueTaskSourceCore 说清楚了!

    目录 1,可用版本与参考资料 2,ValueTask 和 Task 3,编译器如何编译 4,ValueTask 有什么优势 5,ValueTask 创建异步任务 6,IValueTaskSource ...

  4. 【NOIP2015模拟11.2晚】JZOJ8月4日提高组T2 我的天

    [NOIP2015模拟11.2晚]JZOJ8月4日提高组T2 我的天 题目 很久很以前,有一个古老的村庄--xiba村,村子里生活着n+1个村民,但由于历届村长恐怖而且黑暗的魔法统治下,村民们各自过着 ...

  5. Linux 硬盘挂载及开机挂载

    一.分区 主分区.扩展分区.逻辑分区的区别 主分区:包含操作系统启动所必需的文件和数据的硬盘分区,如需在硬盘上安装操作系统,该硬盘必须得有一个主分区 扩展分区:除主分区外的分区,不能直接使用,必须再划 ...

  6. 记一次MySQL出现Waiting for table metadata lock的原因、排查过程与解决方法

    任务背景:将sql文件通过shell直接导入到mysql中执行(还原) bug表现:导入后java项目卡死 过程: 1.网上乱搜一通,无意间看到一篇文章,这篇文章说明了如何开启mysql的genera ...

  7. Linux文件系统简介一(磁盘分区、开机过程、目录结构、文件权限、文件扩展名、目录管理)

    Linux:就是一组软件,一套操作系统=核心+系统呼叫接口层. 1.操作系统 操作系统 = 核心(内核) + 系统呼叫(接口) 操作系统其实也是一组程序,重点在于管理计算机的所有活动以及驱动系统中的所 ...

  8. [Windows] Prism 8.0 入门(下):Prism.Wpf 和 Prism.Unity

    1. Prism.Wpf 和 Prism.Unity 这篇是 Prism 8.0 入门的第二篇文章,上一篇介绍了 Prism.Core,这篇文章主要介绍 Prism.Wpf 和 Prism.Unity ...

  9. Java进阶学习之集合与泛型(1)

    目录 1.集合 1.1.集合是什么 1.2.集合框架结构 1.2.1.Collection 1.2.2.Map 1.3.集合接口实现类 1.3.1.LinkedList 1.3.2.ArrayList ...

  10. neo4j数据库数据转移,从阿里云转移到windows服务器

    1.从阿里云迁移neo4j时需停掉neo4j数据库,在neo4j的bin目录下输入 ./neo4j stop 2.将数据备份到一个文件中 ./neo4j-admin dump --database=g ...