Java基本io和非阻塞io的区别_java nio与bio —— 阻塞IO与非阻塞IO的区别

本文详细探讨了Java的BIO(阻塞IO)和NIO(非阻塞IO)的区别,通过示例展示了它们在socket编程中的应用。非阻塞IO解决了阻塞IO在处理大量不活跃连接时的局限性,但并不意味着其在所有场景下都优于阻塞IO。文章强调了Selector在NIO中的关键作用,用于监听和处理异步事件。
摘要由CSDN通过智能技术生成

在研究Netty源码的过程中,可能是由于对java的nio编程方式不够熟习,关于Selector以及线程那一块,看起来总是觉得差一点。于是,抽空研究了下jdk 的 nio与bio。不得不说,我对于nio与bio的区别了解的不够透彻,而这非常影响对于Netty的学习。这一篇博客会总结下我对nio与bio的学习与了解。(一部分 未完)

为了能更深入的说明两者之间的区别,会从 socket编程 -> nio编程 开始一步步说明。

文章比较长,假如时间紧可以看下阻塞IO的局限性这一段

socket编程

用户端bio编程

9f246192da81c45d940e3e2742821550.png

这里举的例子非常简单了,忽略了异常、超时等等各种情况。实际使用也不大可能仅仅只是读几条数据,就直接关流。这样写主要是为了简单,便于说明API。

简单来说,可以总结这样几点:

1)创立socket(指定要连接的ip及端口)

2)获取并解决输入流

3)关闭socket(真实场景这一步可能是在退出APP后)

服务端bio编程

7d6608cd7932b95376c5ccf2208894ab.png

服务端也非常简单,大致就是:

1)启动一个服务端,监听某一个端口

2)接收用户端连接

3)根据用户端的请求,写入响应(这里用户端请求没有数据)

4)关闭服务器(真实场景可能在服务器中止时触发)

阻塞IO解决多个用户端请求

上述示例只是为了说明服务端API的使用。真实场景当然不可能只有一个用户端连接为了支持多个用户端,我们简单的加个循环。

49d39fc2782f0f2214d3c0117ad1395e.png

1)通过for循环,服务端就变的能解决多个用户端连接

2)注释掉serverSocket.close();是由于上面while(true)最后一条语句怎样样都执行不到了。真实场景一定会有个触发close的地方。

某个连接解决导致服务端无法响应

上述写法,从逻辑上来看改成伪代码如下:

8c1eb03c2ffe1e73cccbffcbf0bbde11.png

这种写法有个非常严重的问题:

因为整个接收请求和解决请求都是在同一个线程里(本示例是主线程)当解决用户端请求这一步发生了阻塞,或者者说慢了,后来的所有连接请求都会被阻塞住。

9922c402afe7a02683fe135fbb7a2d3b.png

处理方法也很简单,启动一个线程去专门解决每一个请求

0ea86c6d6415dc26c98f62e2508fdacf.png

这样解决流程就变成这样了:

a544a2fa76fb51eb46b505908cd7fb06.png

上述方式尽管处理了,某个用户端请求阻塞导致的服务端无法解决连接的问题。但是每次一个新的连接,都会启动一个线程。其余不说,假设有1百万个连接,按照一个连接最少64k来算,64k*1000000 约 61G

(关于一个线程需要多少内存,可以看这个启动一个线程所需内存)按这么算,当连接足够多时,服务端啥都不用干,内存就会被撑爆。

使用线程池解决超多连接

处理方式也很简单,不再每次连接进来都去启动一个线程,而是改成使用线程池

979ae316dd636d8c8fa52f623fc549c2.png

整个流程大致如下:

6d9d395632dc166609dc053a2cc9f287.png

单个线程解决阻塞导致的其余连接无法响应问题

使用线程池意味着,一个线程可能会解决来自多个用户端连接的请求,比方A用户端和B用户端恰好请求都被提交给 线程C,那么结果就是,A用户端的解决慢了,B用户端会连带着响应的特别慢。或者者A用户端的请求阻塞了,B用户端的请求也会连带着阻塞了。

阻塞IO的局限性

那么如何处理这个问题呢?在深入研究NIO和BIO的区别时,我第一反应就是使用非阻塞IO呀。但是,其实我没有弄清楚究竟非阻塞IO和阻塞IO的核心区别是啥。

首先非阻塞IO和阻塞IO最重要的一点区别,我认为是,非阻塞IO的读、写、接收连接是不会产生阻塞的

啥意思呢,首先回到之前写的服务端的示例:

a9907aa99059b51f664470905fedcd3e.png

当时我没有说明一个非常重要的情况,假设一直没有用户端的连接进来,这一步就会阻塞住。而这完全是没有必要的,由于可能在一段时间,根本不会有用户端去连接服务端。我们希望的情况是,用户端有连接了,我们再去accept,打个比如,我再卖菜,我当然希望有人来买菜了,我才去收银。而不是,就在收银台那边干等着,白费时间。

再来看看上述问题——单个线程解决阻塞导致的其余连接无法响应,我们首先要问,为啥会产生阻塞?

第一个起因,由于业务解决很慢。比方读写DB,可能业务就是要读取、写入很多数据,这种慢是没有办法的,无论怎样样,就是需要这么多时间。

第二个起因,socket的读写慢了。由于阻塞IO的读与写都是阻塞的。也就是说,假设服务端开始读了,服务端在用户端发送数据之前会一直阻塞住 啥问题呢,如下图:

902e7d53ab85d7dab32a8a10e41af69f.png

假设用户端A和用户端B都是在一个线程中解决,用户端A已经开始读了(调用了 InputStream.read方法),但是因为没数据,服务端只能阻塞住。用户端B呢,尽管它有数据准备发给服务端,但是由于该线程已经被阻塞住了,所以用户端B的连接也只能等着。

写的场景也一样,假设服务端准备往A写数据,但是呢数据还没准备好,导致用户端B也只能在那等着。(真实场景,这种情况可能比较少。比方收到一个查询db的请求,我们都是从db里读取了数据之后,才会调用write方法写数据的。很少会出现没有数据的问题)

那么比较理想的情况是啥呢?只有用户端有数据发过来了,服务端才去读,才去解决这也就是非阻塞IO。

到了这里,阻塞IO与非阻塞IO一个非常重要的区别应该就清楚了,阻塞IO的读、写、连接都会阻塞整个线程

非阻塞IO的写法public static void main(String[] args) throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8888)); serverSocketChannel.configureBlocking(false); //设置服务端操作都是非阻塞的 Selector selector = Selector.open(); //选择器 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //对用户端的accept事件关心 while (true) { selector.select(); //会阻塞住,直到有事件触发 Set selectionKeys = selector.selectedKeys(); //看下有哪些事件被触发了 System.out.println("selectionKeys:" + selectionKeys); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isAcceptable()) { //用户端 accept被触发了 ServerSocketChannel serverChannel = (ServerSocketChannel)key.channel(); SocketChannel clientChannel = serverChannel.accept(); System.out.println("channel is acceptable"); clientChannel.configureBlocking(false); //用户端channel注册OP_WRITE事件 clientChannel.register(selector, SelectionKey.OP_WRITE); } else if (key.isWritable()) { //用户端可以往里写数据了 System.out.println("channel is writeable"); String data = "hello world\n"; //注意这里的是用户端的channel,由于是使用用户端channel注册OP_WRITE事件 SocketChannel clientChannel = (SocketChannel)key.channel(); ByteBuffer buffer = ByteBuffer.allocate(data.length()); buffer.put(data.getBytes()); buffer.flip(); clientChannel.write(buffer); } key.cancel(); //取消事件 iterator.remove(); } }}

由于这里只是为了说明java nio的写法,所以写的不是很严谨。仅供参考。。实际使用别这么写。

第一次看nio写法时候,很乱,不能了解为啥阻塞IO写起来那么清楚,到了非阻塞IO就变得这么复杂了呢?这里的Selector究竟是啥?这里的SelectionKey又是啥?为啥要判断 acceptable为啥?为啥又要判断writable?

首先牢记一点非阻塞IO的所有操作都是异步的,这意味着什么?首先当我们直接调用 serverSocketChannel.accept(); 很可能直接返回一个null,由于用户端没有连接进来。而阻塞IO会一直等到用户端有连接

在拿服务端读作为例子,当我们直接调用SocketChannel.read()时,可能获取到的就直接是null,而阻塞IO会一直阻塞,直到用户端向服务端发送了数据

这样来看,由于所有请求都是异步,服务端必需要有某种机制,能知道:

1)用户端的连接过来了

2)用户端发送数据过来了

3)用户端可写了

....

而得知这些的方式,就是Selector。通过Selector的select操作,我们能遍历出当前有哪些事件准备好了,比方用户端连接过来了、用户端有数据过来了、可以往用户端发送数据了。

到目前为止,应该能说明为啥非阻塞IO的API设计是这样。更详细的java nio的使用,会在后面的博客里说明。

一个小问题:非阻塞IO是不是肯定比阻塞IO性能要好?

分析到这里,可能会有种感觉,非阻塞IO性能肯定比阻塞IO性能要好。但是其实这样说,并不精确。非阻塞IO处理了服务端有很多不活跃连接 的问题,比方说,用户端连接后,很长一段时间不发送任何请求,这样服务端解决该连接的线程就会一直卡在那里。

但是当连接不多时,并且每个连接都很活跃时,阻塞IO性能可能比非阻塞要好。

总结

这篇博客说明了java 阻塞IO与非阻塞IO的一个非常重要的区别——IO操作能否阻塞。非阻塞IO处理了大量不活跃连接的问题。

问题

非阻塞IO与阻塞IO的区别当然不止这些,对于非阻塞IO的API说明也没有非常详尽。比方非阻塞IO里的buffer是啥?为啥非阻塞IO使用起来,要比阻塞IO复杂的多?后面博客会详细说明这几点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值