生活中例子告诉你:Java NIO复用器Selectors

复用器有能力做就绪事件选择,驱动I/O复用。这样能够让单个线程高效地并发管理多个网络I/O通道。C/C++网络编程库拥有POSIX select() 或者poll()系统调用已经有很多年。但直到Jdk1.4,Java编程人员才能够使用网络就绪选择的功能。

一,用形象的例子理解Selectors

Java中Selectors及其相关类的作用相当于下图多路选择器的功能。通过a、b值的选择达到输入A、B、C、D其中一路可以进行Q输出的功能。
在这里插入图片描述
下图是多路选择器芯片
在这里插入图片描述
如果这个还不够形象。Ok,再来一波教科书上的介绍。

  1. 正如Java NIO1一书中介绍的例子所述。想象一下有三个直通业务窗口的银行。在传统(非复用)场景中,假设每个业务窗口都有一个通道(channel),该通道通向银行的出纳员柜台,每个出纳员柜台都与其他出纳员柜台隔离。这意味着每个通道(channel)需要专用的出纳员(worker thread)。 这种方法不能很好地扩展并且是浪费的。对于每个添加的新通道,都需要一个新的柜员,以及相关的开销,如桌子、椅子、回形针(内存CPU周期上下文切换)等。当事情进展缓慢时,这些资源往往是闲置的。
  2. 现在想象一个不同的场景,其中通道连接到银行内的出纳员柜台。 该柜台有三个插槽,业务(data buffer)到达,每个插槽都有一个指示灯(selection key),当业务位于插槽中时,指示灯亮起。 再想象一下出纳员(worker thread)有一个手机,并花费尽可能多的时间看手机。在十分钟后,出纳员向上看指示灯(调用select())以确定是否有任何通道准备就绪(readiness selection)。当直通车道(通道)处于空闲状态,出纳员(worker thread)可以执行另一项任务。但在需要注意时仍能及时响应。
  3. 一个更准确的例子,自己可以想象一下现在的餐厅,老板、服务员、顾客和厨师的关系。

二,Selectors 基础

  1. 用对象selector注册一个至多个可选择的通道(channel)。一个key值表示返回的一个通道(channel)和一个选择(selector)的关系。Selection keys能够记住你对每个通道感兴趣的键值。Selection keys也能够追踪通道(channel)现在能够执行的兴趣操作。在对象selector上调用select()时,通过检查使用该选择器注册的所有通道来更新关联的键。你能够获得key值set集合,进而迭代这些key值来服务在上一次调用select()时产生的所有已经就绪通道(channel)。
  2. 这是从上层看了鸟瞰了Selectors基础,接下来进行底层剖析。
  3. 在最基本的层面上,Selectors提供了询问通道是否已准备好执行您感兴趣的I/O操作的功能。例如,可以询问SocketChannel对象是否有任何准备好读取的字节,或者我们可能想知道ServerSocketChannel是否有任何准备接受的传入连接。
  4. SelectorsSelectableChannel对象一起使用时提供此服务,但故事的内容不止于此。readiness selection的真正力量是可以同时检查潜在的大量通道的准备情况。调用者可以轻松确定几个通道(channel)中的哪一个已准备就绪。可选地,调用线程可以要求其进入休眠状态,直到用Selectors注册的一个或多个通道准备就绪,或者可以周期性地轮询选择器以查看自上次检查以来是否有任何准备就绪。如果你想需要一个必须管理大量并发连接的Web服务器,很容易想象如何充分利用这些功能。
  5. 乍一看,似乎可以单独使用非阻塞模式模拟readiness selection,但实际上并非如此。非阻塞模式将执行请求的操作或指示它不执行。这在语义上与确定是否可以执行某种类型的操作不同。例如,如果尝试非阻塞读取并且成功,则不仅可以发现read(),还可以读取一些数据。然后,必须对该数据执行某些操作。
  6. 这有效地防止了将检查通道准备就绪的代码与处理数据的代码分开,至少没有明显的复杂性。即使可以简单地询问每个通道(channel)是否准备就绪,这仍然会有问题,因为你的代码或库包中的某些代码需要遍历所有候选通道channel)并依次检查每个通道(channel)。这将导致每个通道(channel)至少有一个系统调用来查看其准备情况,这可能很昂贵,但主要问题是检查不是原子性的(多线程应用)。列表(list)中的早期通道(channel)可以在检查后准备就绪,但在下次轮询(poll)之前不会知道它是否可用。最糟糕的是,你别无选择,只能继续轮询列表(list)。当你感兴趣的频道准备就绪时,你将无法立即获得通知。
  7. 这就是为什么监视多个socket的传统Java解决方案是为每个socket创建一个线程,并允许线程在read()中阻塞,直到数据可用。这有效地让每个被阻塞的线程成socket监视器,并且JVM的线程调度程序成为通知机制。管理所有这些线程的复杂性和性能成本,对于程序员和JVM来说,随着线程数量的增长而迅速失控。
  8. 真正的readiness selection必须由操作系统完成。操作系统执行的最重要功能之一是处理I/O请求并在数据准备就绪时通知进程。因此,将此功能委托给操作系统才有意义。Selector类提供抽象,通过该抽象,Java代码可以便携地从底层操作系统请求readiness selection服务。

三,Selectors 相关类

下面来理清Selectors 相关类怎样实现交互完成readiness selection功能的。主要有三个相关类。

  1. Selector:该类管理有关一组注册通道及其readiness selection的信息。 通道向选择器注册,并且可以要求选择器更新当前向其注册的通道(channel)的准备状态。这样做时,调用线程可以指示它暂停,直到其中一个已注册的通道准备就绪。
  2. SelectableChannel:此抽象类提供了实现通道选择性所需的常用方法。它是支持readiness selection功能的所有通道类的超类。FileChannel对象不可选,因为它不从SelectableChannel扩展。所有套接字通道类都是可选的,以及从Pipe对象获得的通道。可以使用Selector对象注册SelectableChannel对象,并指示该通道上哪些操作对该选择器感兴趣。通道注册到多个选择器上,但每个选择器只能注册一次。
  3. SelectionKey:该类封装特定通道和特定选择器之间的注册关系。SelectionableChannel.register()返回SelectionKey对象,并用作表示注册的标记。SelectionKey对象包含两个位(编码为整数),指示注册者对哪些通道操作感兴趣以及通道准备执行哪些操作。

  1. https://item.jd.com/1105633702.html ↩︎

发布了80 篇原创文章 · 获赞 32 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_40657528/article/details/90376111