Java NIO 中的非阻塞究竟体现在哪里?

Java NIO 中的非阻塞究竟体现在哪里?

  • Reactor 模式
  • 异步非阻塞可以基于 Java BIO 实现吗?
  • Java NIO 中的非阻塞究竟体现在哪里?

  很多人应该对“Java NIO 是非阻塞的 I/O”这一信条熟记于心,但其中的有些人可能经过实践之后却产生这样的疑惑:Java NIO 明明是非阻塞的 I/O,但 Java NIO 中无论是 Channel 还是 Selector 的方法却是阻塞的,其中的一个被称为设置 Channel 为非阻塞的方法 XXXChannel.configureBlocking(false) 看起来并没有所言的拥有“将 Channel 变为非阻塞的”的作用,那究竟凭什么说 Java NIO 是非阻塞的 I/O 呢?

  “Java NIO 是非阻塞的 I/O”,这一论断实际上是谬论,Java NIO 中的 N,并不是指的是 Non-blocking(非阻塞)的意思,而是 New(新)的意思。Java NIO 只是相对于原来所谓 Java BIO 的一个升级,并不能就说它就是一个非阻塞的 I/O。不过,相对于 Java BIO,Java NIO 确实有一些非阻塞的特性。具体来说,Java NIO 的非阻塞,是相对于连接的非阻塞,而不是指方法调用时的非阻塞

  同一个词在不同领域的含义会有所变化。笔者曾经写了若干博客来探究 阻塞非阻塞 等的含义,可见:

  • 同步与异步、并行与并发、阻塞与挂起:https://blog.csdn.net/wangpaiblog/article/details/116114098
  • 同步阻塞、同步非阻塞、异步阻塞、异步非阻塞之间的区别:https://blog.csdn.net/wangpaiblog/article/details/117236684

  一般情况下的 阻塞非阻塞 指的就是笔者上面博客中的意思,不过,Java NIO 的非阻塞指的并不是上面博客中的意思。前面有言,Java NIO 的非阻塞,是相对于连接的非阻塞,而不是指方法调用时的非阻塞。这是什么意思呢?一般来说,方法的非阻塞,指的是调用该方法时,如果该方法能立刻返回而不阻塞调用该方法的线程,那么该方法就是非阻塞的。而 Java NIO 的非阻塞,是相对于连接的非阻塞。当通信连接就断开时,Java NIO 中的 处理线程 也不会阻塞到某一个连接中,而是根据所有连接中 I/O 事件的有无作出处理。下面来详细解释这个问题。

Reactor 模式

  在解释上面的问题之前,需要解释什么是 Reactor 模式。Reactor 模式并不是什么很神秘的东西。一般来说,通信有以下两个关键步骤:一是建立连接,二是进行数据的传输。一般对通信过程进行优化,也就是基于这两个关键点进行优化。

  建立连接通常就是 握手 的过程。在向对方传输数据之前,必须要经对方同意,这就是握手。如果建立连接就不成功,后面的数据传输也不能进行。对于服务端来讲,它需要与众多客户端通信,建立连接的成功数将影响它的吞吐量。对于服务端,当然是希望它的吞吐量越高越好。我们知道数据传输通常相对更耗时,为了避免对建立连接造成影响,最好是让服务端使用一个与数据传输不同的线程来完成建立连接的工作。

  对于服务端来讲,它需要与众多客户端通信。如果正好需要与多个客户端同时进行数据传输,就只能开启多个线程来完成,且线程数与连接数相等。如果线程数小于连接数,则有些客户端需要等待。

  Reactor 模式就是基于建立连接与具体服务之间线程分离的模式。在 Reactor 模式中,会有一个线程负责与所有客户端建立连接,这个线程通常称之为 Reactor。然后在建立连接之后,Reactor 线程 会使用其它线程(可以有多个)来处理与每一个客户端之间的数据传输,这个(些)线程通常称之为 Handler

  由于服务端需要与多个客户端通信,它的通信是一对多的关系,所以它需要使用 Reactor 模式。对客户端,它只需要与服务端通信,它的通信是一对一的关系,所以它不需要使用 Reactor 模式。也就是说,对客户端来讲,它不需要进行建立连接与传输数据之间的线程分离。

  更多关于 Reactor 模式的信息,可见笔者的另一篇博客:

  什么是 Reactor 模式?:
https://blog.csdn.net/wangpaiblog/article/details/124580590

异步非阻塞可以基于 Java BIO 实现吗?

  这里有一种很多人根深蒂固的误区。Java BIO 是一种阻塞的 I/O,有人就此当作 BIO 效率低下的理由。但这种说法是不对的。一方面,阻塞与效率低下没有必然联系。在操作系统层面上,线程阻塞时并不会占用 CPU 时间。也就是说,当一个线程阻塞时,操作系统会转而执行其它线程,需要付出的代价是线程切换,而不是线程阻塞的持续时间。而此处,阻塞确实导致 BIO 效率低下,但这是因为导致服务端 BIO 线程阻塞的,是客户端的连接。对于 BIO 而言,如果客户端一方不关闭连接,则此连接将一直维持。前面有言,一个连接必须开启一个线程来处理。如果客户端不关闭连接,然后又不传输数据,这就会浪费处理此连接的线程的资源。

  另一方面,借助多线程技术可以实现阻塞到非阻塞的转化,借助队列技术可以实现同步到异步的转化。因此,即使 BIO 是阻塞的,只需要借助多线程和队列对 BIO 简单封装一层,就可以实现一个异步非阻塞的 I/O。如果 NIO 可以直接由 BIO 实现,那么 JDK 将不会为 NIO 提供专门的包,因为 JDK 只会提供基础的 API。

  Java NIO 的优势要通过与 BIO 比较才能体现。对于 BIO 而言,即便是借助 Reactor 模式和线程池技术,仍然有很大的性能瓶颈。在 Reactor 模式下,BIO 也可以使用一个 Reactor 线程建立与所有客户端的连接,然后将具体每一个客户端连接推送到 Handler 线程池中。但由于线程池的容量是有限的,而对于 BIO 而言,如果客户端一方不关闭连接,则此连接将一直维持。又因为一个连接必须开启一个线程来处理。如果客户端不关闭连接,然后又不传输数据,线程池的资源将一直被无用事项所占据,其它客户端的业务将不能得到处理。

  在这种情形下,如果同时使用另一个 监视者线程 来管理 Handler 线程,就可以基于 Java BIO 来实现一套异步非阻塞的 I/O API。具体来说,如果每个 Handler 线程处理完业务、重新进入阻塞(等待数据传输)的状态之前开启计时器来计算等待时间。对于监视者线程,它的职责是发现哪个 Handler 线程阻塞时间过长。如果阻塞时间过长,就强制断开此 Handler 线程与客户端的连接,然后把连接资源留给下一个客户端。这样一来,即便底层是基于 Java BIO,这种方式也可以实现一套异步非阻塞的 I/O API。因此,非阻塞绝不是 Java NIO 优于 BIO 的根本理由。Java NIO 优于 BIO 的原因是 Java NIO 底层使用了 I/O 多路复用技术,而该技术需要底层操作系统的支持。

Java NIO 中的非阻塞究竟体现在哪里?

  对于 Java NIO,在 Reactor 模式下,客户端的连接不会直接与服务端的 Handler 线程相绑定。Java NIO 中 Channel 的非阻塞就体现在这里。为什么 NIO 中客户端的连接不会像 BIO 一样直接与服务端的 Handler 线程相绑定呢?因为 Channel 对连接是非阻塞的,当客户端不关闭连接,但又不传输数据时,Channel 将不会因此而阻塞,这样同一个 Handler 线程就可以处理其它客户端的请求,而避免了像 BIO 一样一直等到连接关闭。

  具体来讲,NIO 的这个特性是由 Reactor 线程中的 Selector 与 Channel 共同来完成的。Channel 用于建立连接与通信渠道,而 Selector 可以做到的是(这需要操作系统的支持),只有发生了有意义的 I/O 事件时,它才会令 Handler 线程处理这个事件,这样一来,当客户端的连接依然存在但又没有实际数据传输时,Handle 线程的资源将不会被占用。另一方面,Channel 对连接是非阻塞的,这样一来,当与客户端的一次数据传输完成,而客户端又没有关闭连接时,Channel 可以在这次数据传输完成时就返回,然后本 Handler 线程的资源就又可以空出来处理其它客户端的数据传输,这就是所谓的 I/O 多路复用

  那么,前面为什么又说 Channel 的方法是阻塞的呢?这不矛盾。前面有言,Java NIO 的非阻塞,是相对于连接的非阻塞,而不是指方法调用时的非阻塞。当 Channel 进行一次数据传输时,它会阻塞到数据传输完成。而它的非阻塞体现在,当本次数据传输完成,而与客户端的连接没有关闭时,Channel 的方法可以立刻返回而不会持续阻塞到连接关闭。

  在 Java NIO 中,除了 Channel 之外,Selector 的方法也是阻塞的,这会不会因此带来什么严重的效率瓶颈呢?不会。因为 Selector 隶属于 Reactor 线程中,而 Reactor 线程不做其它的事情,就只是监听 I/O 事件而已。它是因为监听 I/O 事件而陷入阻塞,当有监听到有意义的 I/O 事件时,它就会解除阻塞。因此,它的阻塞是合情合理的。

你可能感兴趣的:(概念辨析/科普,Java,Java,NIO,非阻塞,BIO,Reactor,模式,I/O,多路复用)