写点什么

面试常见 IO 问答知识点

作者:浅羽技术
  • 2022 年 8 月 29 日
    四川
  • 本文字数:4053 字

    阅读完需:约 13 分钟

面试常见IO问答知识点

bio,nio,aio 的区别;

IO 的方式通常分为几种,同步阻塞的 BIO、同步非阻塞的 NIO、异步非阻塞的 AIOBIO


 在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。
复制代码


NIO


NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题: 在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。如果客户端的请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。
NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。 也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
复制代码


BIO 与 NIO 一个比较重要的不同,是我们使用 BIO 的时候往往会引入多线程,每个连接一个单独的线程;而 NIO 则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。


  NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,    当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题。
复制代码


HTTP/1.1 出现后,有了 Http 长连接,这样除了超时和指明特定关闭的 http header 外,这个链接是一直打开的状态的,这样在 NIO 处理中可以进一步的进化,在后端资源中可以实现资源池或者队列,当请求来的话,开启的线程把请求和请求数据传送给后端资源池或者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接的哪个请求等),


这样前面的线程还是可以去接受其他的请求,而后端的应用的处理只需要执行队列里面的就可以了,这样请求处理和后端应用是异步的.当后端处理完,到全局地方得到现场,产生响应,这个就实现了异步处理。


AIO 与 NIO 不同

当进行读写操作时,只须直接调用 API 的 read 或 write 方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write 方法都是异步的,完成后会主动调用回调函数。


在 JDK1.7 中,这部分内容被称作 NIO.2,主要在 Java.nio.channels 包下增加了下面四个异步通道:AsynchronousSocketChannelAsynchronousServerSocketChannelAsynchronousFileChannelAsynchronousDatagramChannel 其中的 read/write 方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。


BIO 是一个连接一个线程。NIO 是一个请求一个线程。AIO 是一个有效请求一个线程。先来个例子理解一下概念,以银行取款为例:同步 : 自己亲自出马持银行卡到银行取钱(使用同步 IO 时,Java 自己处理 IO 读写);异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步 IO 时,Java 将 IO 读写委托给 OS 处理,需要将数据缓冲区地址和大小传给 OS(银行卡和密码),OS 需要支持异步 IO 操作 API);


阻塞 : ATM 排队取款,你只能等待(使用阻塞 IO 时,Java 调用会一直阻塞到读写完成才返回);非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞 IO 时,如果不能读写 Java 调用会马上返回,当 IO 事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)


Java 对 BIO、NIO、AIO 的支持:


Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。


java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。


Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理,BIO、NIO、AIO 适用场景分析:BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。


NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。


另外,I/O 属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。在高性能的 I/O 设计中,有两个比较著名的模式 Reactor 和 Proactor 模式,其中 Reactor 模式用于同步 I/O,而 Proactor 运用于异步 I/O 操作。


在比较这两个模式之前,我们首先的搞明白几个概念,什么是阻塞和非阻塞,什么是同步和异步,同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发 IO 操作并等待或者轮询的去查看 IO 操作是否就绪,而异步是指用户进程触发 IO 操作以后便开始做自己的事情,而当 IO 操作已经完成的时候会得到 IO 完成的通知。


而阻塞和非阻塞是针对于进程在访问数据的时候,根据 IO 操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。


一般来说 I/O 模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞 IO


同步阻塞 IO:在此种方式下,用户进程在发起一个 IO 操作以后,必须等待 IO 操作的完成,只有当真正完成了 IO 操作以后,用户进程才能运行。


JAVA 传统的 IO 模型属于此种方式!同步非阻塞 IO:在此种方式下,用户进程发起一个 IO 操作以后边可返回做其它事情,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。


其中目前 JAVA 的 NIO 就属于同步非阻塞 IO。异步阻塞 IO:此种方式下是指应用发起一个 IO 操作以后,不等待内核 IO 操作的完成,等内核完成 IO 操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问 IO 是否完成,那么为什么说是阻塞的呢?


因为此时是通过 select 系统调用来完成的,而 select 函数本身的实现方式是阻塞的,而采用 select 函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!


异步非阻塞 IO:在此种模式下,用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的 IO 读写操作,因为真正的 IO 读取或者写入操作已经由内核完成了。目前 Java 中还没有支持此种 IO 模型

nio 框架:dubbo 的实现原理;

client 一个线程调用远程接口,生成一个唯一的 ID(比如一段随机字符串,UUID 等),Dubbo 是使用 AtomicLong 从 0 开始累计数字的将打包的方法调用信息(如调用的接口名称,方法名称,参数值列表等),和处理结果的回调对象 callback,全部封装在一起,组成一个对象 object 向专门存放调用信息的全局 ConcurrentHashMap 里面 put(ID, object)将 ID 和打包的方法调用信息封装成一对象 connRequest,使用 IoSession.write(connRequest)异步发送出去当前线程再使用 callback 的 get()方法试图获取远程返回的结果,在 get()内部,则使用 synchronized 获取回调对象 callback 的锁, 再先检测是否已经获取到结果


如果没有,然后调用 callback 的 wait()方法,释放 callback 上的锁,让当前线程处于等待状态。服务端接收到请求并处理后,将结果(此结果中包含了前面的 ID,即回传)发送给客户端,客户端 socket 连接上专门监听消息的线程收到消息,分析结果,取到 ID,再从前面的 ConcurrentHashMap 里面 get(ID),从而找到 callback,将方法调用结果设置到 callback 对象里。监听线程接着使用 synchronized 获取回调对象 callback 的锁(因为前面调用过 wait(),那个线程已释放 callback 的锁了),再 notifyAll(),唤醒前面处于等待状态的线程继续执行(callback 的 get()方法继续执行就能拿到调用结果了),至此,整个过程结束。


当前线程怎么让它“暂停”,等结果回来后,再向后执行?答:先生成一个对象 obj,在一个全局 map 里 put(ID,obj)存放起来,再用 synchronized 获取 obj 锁,再调用 obj.wait()让当前线程处于等待状态,然后另一消息监听线程等到服务端结果来了后,再 map.get(ID)找到 obj,再用 synchronized 获取 obj 锁,再调用 obj.notifyAll()唤醒前面处于等待状态的线程。


正如前面所说,Socket 通信是一个全双工的方式,如果有多个线程同时进行远程方法调用,这时建立在 client server 之间的 socket 连接上会有很多双方发送的消息传递,前后顺序也可能是乱七八糟的,server 处理完结果后,将结果消息发送给 client,client 收到很多消息,怎么知道哪个消息结果是原先哪个线程调用的?答:使用一个 ID,让其唯一,然后传递给服务端,再服务端又回传回来,这样就知道结果是原先哪个线程的了。

发布于: 刚刚阅读数: 3
用户头像

浅羽技术

关注

才疏学浅,习习而为,编程羽录,与你同行。 2019.02.26 加入

分享一些计算机信息知识、理论技术、工具资源、软件介绍、后端开发、面试、工作感想以及生活随想等一系列文章。

评论

发布
暂无评论
面试常见IO问答知识点_io_浅羽技术_InfoQ写作社区