最新公告
  • 新注册用户请前往个人中心绑定邮箱以便接收相关凭证邮件!!!点击前往个人中心
  • 10个最高频的Java NIO面试题剖析!

     

    首先我们分别画图来看看,BIO、NIO、AIO,分别是什么?
     
    BIO:传统的网络通讯模型,就是BIO,同步阻塞IO
     
    它其实就是服务端创建一个ServerSocket, 然后就是客户端用一个Socket去连接服务端的那个ServerSocket, ServerSocket接收到了一个的连接请求就创建一个Socket和一个线程去跟那个Socket进行通讯。
     
    接着客户端和服务端就进行阻塞式的通信,客户端发送一个请求,服务端Socket进行处理后返回响应。
     
    在响应返回前,客户端那边就阻塞等待,上门事情也做不了。
     
    这种方式的缺点:每次一个客户端接入,都需要在服务端创建一个线程来服务这个客户端
     
    这样大量客户端来的时候,就会造成服务端的线程数量可能达到了几千甚至几万,这样就可能会造成服务端过载过高,最后崩溃死掉。
    BIO模型图:

    Acceptor:
    传统的IO模型的网络服务的设计模式中有俩种比较经典的设计模式:一个是多线程, 一种是依靠线程池来进行处理。
     
    如果是基于多线程的模式来的话,就是这样的模式,这种也是Acceptor线程模型。

    NIO:

    NIO是一种同步非阻塞IO, 基于Reactor模型来实现的。
     
    其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。
     
    这里的核心就是非阻塞,就那个selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞,直接就会进来,大不了就是等待一下排着队而已。
     
    这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。
    NIO:模型图

    Reactor模型:

    AIO

    AIO:异步非阻塞IO,基于Proactor模型实现。
     
    每个连接发送过来的请求,都会绑定一个Buffer,然后通知操作系统去完成异步的读,这个时间你就可以去做其他的事情
     
    等到操作系统完成读之后,就会调用你的接口,给你操作系统异步读完的数据。这个时候你就可以拿到数据进行处理,将数据往回写
     
    在往回写的过程,同样是给操作系统一个Buffer,让操作系统去完成写,写完了来通知你。
     
    这俩个过程都有buffer存在,数据都是通过buffer来完成读写。
     
    这里面的主要的区别在于将数据写入的缓冲区后,就不去管它,剩下的去交给操作系统去完成。
     
    操作系统写回数据也是一样,写到Buffer里面,写完后通知客户端来进行读取数据。
    AIO:模型图

    聊完了BIO,NIO,AIO的区别之后,现在我们再结合这三个模型来说下同步和阻塞的一些问题。
    同步阻塞
    为什么说BIO是同步阻塞的呢?
    其实这里说的不是针对网络通讯模型而言,而是针对磁盘文件读写IO操作来说的。
    因为用BIO的流读写文件,例如FileInputStrem,是说你发起个IO请求直接hang死,卡在那里,必须等着搞完了这次IO才能返回。

    同步非阻塞:

    为什么说NIO为啥是同步非阻塞?
    因为无论多少客户端都可以接入服务端,客户端接入并不会耗费一个线程,只会创建一个连接然后注册到selector上去,这样你就可以去干其他你想干的其他事情了
     
    一个selector线程不断的轮询所有的socket连接,发现有事件了就通知你,然后你就启动一个线程处理一个请求即可,这个过程的话就是非阻塞的。
     
    但是这个处理的过程中,你还是要先读取数据,处理,再返回的,这是个同步的过程。

    异步非阻塞

    为什么说AIO是异步非阻塞?
     
    通过AIO发起个文件IO操作之后,你立马就返回可以干别的事儿了,接下来你也不用管了,操作系统自己干完了IO之后,告诉你说ok了
     
    当你基于AIO的api去读写文件时, 当你发起一个请求之后,剩下的事情就是交给了操作系统
     
    当读写完成后, 操作系统会来回调你的接口, 告诉你操作完成
     
    在这期间不需要等待, 也不需要去轮询判断操作系统完成的状态,你可以去干其他的事情。
     
    同步就是自己还得主动去轮询操作系统,异步就是操作系统反过来通知你。所以来说, AIO就是异步非阻塞的。

    NIO核心组件详细讲解

    学习NIO先来搞清楚一些相关的概念,NIO通讯有哪些相关组件,对应的作用都是什么,之间有哪些联系?

    多路复用机制实现Selector

     
    首先我们来了解下传统的Socket网络通讯模型。
    传统Socket通讯原理图

    为什么传统的socket不支持海量连接?
    每次一个客户端接入,都是要在服务端创建一个线程来服务这个客户端的
     
    这会导致大量的客户端的时候,服务端的线程数量可能达到几千甚至几万,几十万,这会导致服务器端程序负载过高,不堪重负,最终系统崩溃死掉。
    接着来看下NIO是如何基于Selector实现多路复用机制支持的海量连接。
    NIO原理图

    多路复用机制是如何支持海量连接?
    NIO的线程模型对Socket发起的连接不需要每个都创建一个线程,完全可以使用一个Selector来多路复用监听N多个Channel是否有请求,该请求是对应的连接请求,还是发送数据的请求
     
    这里面是基于操作系统底层的Select通知机制的,一个Selector不断的轮询多个Channel,这样避免了创建多个线程
     
    只有当莫个Channel有对应的请求的时候才会创建线程,可能说1000个请求, 只有100个请求是有数据交互的
     
    这个时候可能server端就提供10个线程就能够处理这些请求。这样的话就可以避免了创建大量的线程。

    NIO如何通过Buffer来缓冲数据的

    NIO中的Buffer是个什么东西 ?

    学习NIO,首当其冲就是要了解所谓的Buffer缓冲区,这个东西是NIO里比较核心的一个部分
     
    一般来说,如果你要通过NIO写数据到文件或者网络,或者是从文件和网络读取数据出来此时就需要通过Buffer缓冲区来进行。Buffer的使用一般有如下几个步骤:
    写入数据到Buffer,调用flip()方法,从Buffer中读取数据,调用clear()方法或者compact()方法。
    Buffer中对应的Position, Mark, Capacity,Limit都啥?

    • capacity:缓冲区容量的大小,就是里面包含的数据大小。

    • limit:对buffer缓冲区使用的一个限制,从这个index开始就不能读取数据了。

    • position:代表着数组中可以开始读写的index, 不能大于limit。

    • mark:是类似路标的东西,在某个position的时候,设置一下mark,此时就可以设置一个标记
      后续调用reset()方法可以把position复位到当时设置的那个mark上。去把position或limit调整为小于mark的值时,就丢弃这个mark
      如果使用的是Direct模式创建的Buffer的话,就会减少中间缓冲直接使用DirectorBuffer来进行数据的存储。

    如何通过Channel和FileChannel读取Buffer数据写入磁盘的

    NIO中,Channel是什么? 
    Channel是NIO中的数据通道,类似流,但是又有些不同
     
    Channel既可从中读取数据,又可以从写数据到通道中,但是流的读写通常是单向的。
     
    Channel可以异步的读写。Channel中的数据总是要先读到一个Buffer中,或者从缓冲区中将数据写到通道中。

    FileChannel的作用是什么?
    Buffer有不同的类型,同样Channel也有好几个类型。
     
    • FileChannel
    • DatagramChannel
    • SocketChannel
    • ServerSocketChannel
     
    这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。而FileChannel就是文件IO对应的管道, 在读取文件的时候会用到这个管道。
     
    下面给一个简单的NIO实现读取文件的Demo代码
     
    1. publicclassFileChannelDemo1{
    2. publicstaticvoid main(String[] args)throwsException{
    3. // 构造一个传统的文件输出流
    4. FileOutputStreamout=newFileOutputStream(
    5. "F:\development\tmp\hello.txt");
    6. // 通过文件输出流获取到对应的FileChannel,以NIO的方式来写文件
    7. FileChannel channel =out.getChannel();
    8. // 将数据写入到Buffer中
    9. ByteBuffer buffer =ByteBuffer.wrap("hello world".getBytes());
    10. // 通过FileChannel管道将Buffer中的数据写到输出流中去,持久化到磁盘中去
    11. channel.write(buffer);
    12. channel.close();
    13. out.close();
    14. }
    15. }

    NIOServer端和Client端代码案例

    最后,给大家一个NIO客户端和服务端示例代码,简单感受下NIO通讯的方式。
    • NIO通讯Client端
    1. import java.io.IOException;
    2. import java.net.InetSocketAddress;
    3. import java.nio.ByteBuffer;
    4. import java.nio.channels.SelectionKey;
    5. import java.nio.channels.Selector;
    6. import java.nio.channels.SocketChannel;
    7. import java.util.Iterator;
    8. publicclassNIOClient{
    9. publicstaticvoid main(String[] args){
    10. for(int i =0; i <10; i++){
    11. newWorker().start();
    12. }
    13. }
    14. staticclassWorkerextendsThread{
    15. @Override
    16. publicvoid run(){
    17. SocketChannel channel =null;
    18. Selector selector =null;
    19. try{
    20. // SocketChannel,一看底层就是封装了一个Socket
    21. channel =SocketChannel.open();// SocketChannel是连接到底层的Socket网络
    22. // 数据通道就是负责基于网络读写数据的
    23. channel.configureBlocking(false);
    24. channel.connect(newInetSocketAddress("localhost",9000));
    25. // 后台一定是tcp三次握手建立网络连接
    26. selector =Selector.open();
    27. // 监听Connect这个行为
    28. channel.register(selector,SelectionKey.OP_CONNECT);
    29. while(true){
    30. // selector多路复用机制的实现 循环去遍历各个注册的Channel
    31. selector.select();
    32. Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
    33. while(keysIterator.hasNext()){
    34. SelectionKey key =(SelectionKey) keysIterator.next();
    35. keysIterator.remove();
    36. // 如果发现返回的时候一个可连接的消息 走到下面去接受数据
    37. if(key.isConnectable()){ channel =(SocketChannel) key.channel();
    38. if(channel.isConnectionPending()){
    39. channel.finishConnect();
    40. // 接下来对这个SocketChannel感兴趣的就是人家server给你发送过来的数据了
    41. // READ事件,就是可以读数据的事件
    42. // 一旦建立连接成功了以后,此时就可以给server发送一个请求了
    43. ByteBuffer buffer =ByteBuffer.allocate(1024);
    44. buffer.put("你好".getBytes());
    45. buffer.flip();
    46. channel.write(buffer);
    47. }
    48. channel.register(selector,SelectionKey.OP_READ);
    49. }
    50. // 这里的话就时候名服务器端返回了一条数据可以读了
    51. elseif(key.isReadable()){ channel =(SocketChannel) key.channel();
    52. // 构建一个缓冲区
    53. ByteBuffer buffer =ByteBuffer.allocate(1024);
    54. // 把数据写入buffer,position推进到读取的字节数数字
    55. int len = channel.read(buffer);
    56. if(len >0){
    57. System.out.println("["+Thread.currentThread().getName()
    58. +"]收到响应:"+newString(buffer.array(),0, len));
    59. Thread.sleep(5000);
    60. channel.register(selector,SelectionKey.OP_WRITE);
    61. }
    62. }elseif(key.isWritable()){
    63. ByteBuffer buffer =ByteBuffer.allocate(1024);
    64. buffer.put("你好".getBytes());
    65. buffer.flip();
    66. channel =(SocketChannel) key.channel();
    67. channel.write(buffer);
    68. channel.register(selector,SelectionKey.OP_READ);
    69. }
    70. }
    71. }
    72. }catch(Exception e){
    73. e.printStackTrace();
    74. }finally{
    75. if(channel !=null){
    76. try{
    77. channel.close();
    78. }catch(IOException e){
    79. e.printStackTrace();
    80. }
    81. }
    82. if(selector !=null){
    83. try{
    84. selector.close();
    85. }catch(IOException e){
    86. e.printStackTrace();
    87. }
    88. }
    89. }
    90. }
    91. }
    92. }
    • NIO通讯Server端
    1. import java.io.IOException;
    2. import java.net.InetSocketAddress;
    3. import java.nio.ByteBuffer;
    4. import java.nio.channels.ClosedChannelException;
    5. import java.nio.channels.SelectionKey;
    6. import java.nio.channels.Selector;
    7. import java.nio.channels.ServerSocketChannel;
    8. import java.nio.channels.SocketChannel;
    9. import java.util.Iterator;
    10. import java.util.concurrent.ExecutorService;
    11. import java.util.concurrent.Executors;
    12. import java.util.concurrent.LinkedBlockingQueue;
    13. publicclassNIOServer{
    14. privatestaticSelector selector;
    15. privatestaticLinkedBlockingQueue<SelectionKey> requestQueue;
    16. privatestaticExecutorService threadPool;
    17. publicstaticvoid main(String[] args){
    18. init();
    19. listen();
    20. }
    21. privatestaticvoid init(){
    22. ServerSocketChannel serverSocketChannel =null;
    23. try{
    24. selector =Selector.open();
    25. serverSocketChannel =ServerSocketChannel.open();
    26. // 将Channel设置为非阻塞的 NIO就是支持非阻塞的
    27. serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(newInetSocketAddress(9000),100);
    28. // ServerSocket,就是负责去跟各个客户端连接连接请求的
    29. serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
    30. // 就是仅仅关注这个ServerSocketChannel接收到的TCP连接的请求
    31. }catch(IOException e){
    32. e.printStackTrace();
    33. }
    34. requestQueue =newLinkedBlockingQueue<SelectionKey>(500);
    35. threadPool =Executors.newFixedThreadPool(10);
    36. for(int i =0; i <10; i++){
    37. threadPool.submit(newWorker());
    38. }
    39. }
    40. privatestaticvoid listen(){
    41. while(true){
    42. try{
    43. selector.select();
    44. Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
    45. while(keysIterator.hasNext()){
    46. SelectionKey key =(SelectionKey) keysIterator.next();
    47. // 可以认为一个SelectionKey是代表了一个请求
    48. keysIterator.remove();
    49. handleRequest(key);
    50. }
    51. }
    52. catch(Throwable t){
    53. t.printStackTrace();
    54. }
    55. }
    56. }
    57. privatestaticvoid handleRequest(SelectionKey key)
    58. throwsIOException,ClosedChannelException{
    59. // 后台的线程池中的线程处理下面的代码逻辑
    60. SocketChannel channel =null;
    61. try{
    62. // 如果说这个Key是一个acceptable,也就是一个连接请求
    63. if(key.isAcceptable()){
    64. ServerSocketChannel serverSocketChannel =(ServerSocketChannel) key.channel();
    65. // 调用accept这个方法 就可以进行TCP三次握手了
    66. channel = serverSocketChannel.accept();
    67. // 握手成功的话就可以获取到一个TCP连接好的SocketChannel
    68. channel.configureBlocking(false);
    69. channel.register(selector,SelectionKey.OP_READ);
    70. // 仅仅关注这个READ请求,就是人家发送数据过来的请求
    71. }
    72. // 如果说这个key是readable,是个发送了数据过来的话,此时需要读取客户端发送过来的数据
    73. elseif(key.isReadable()){
    74. channel =(SocketChannel) key.channel();
    75. ByteBuffer buffer =ByteBuffer.allocate(1024);
    76. int count = channel.read(buffer);
    77. // 通过底层的socket读取数据,写buffer中,position可能就会变成21之类的
    78. // 你读取到了多少个字节,此时buffer的position就会变成多少
    79. if(count >0){
    80. // 准备读取刚写入的数据,就是将limit设置为当前position,将position设置为0,丢弃mark。一般就是先写入数据,接着准备从0开始读这段数据,就可以用flip
    81. // position = 0,limit = 21,仅仅读取buffer中,0~21这段刚刚写入进去的数据
    82. buffer.flip();
    83. System.out.println("服务端接收请求:"+newString(buffer.array(),0, count));
    84. channel.register(selector,SelectionKey.OP_WRITE);
    85. }
    86. }elseif(key.isWritable()){
    87. ByteBuffer buffer =ByteBuffer.allocate(1024);
    88. buffer.put("收到".getBytes());
    89. buffer.flip();
    90. channel =(SocketChannel) key.channel();
    91. channel.write(buffer);
    92. channel.register(selector,SelectionKey.OP_READ);
    93. }
    94. }
    95. catch(Throwable t){
    96. t.printStackTrace();
    97. if(channel !=null){
    98. channel.close();
    99. }
    100. }
    101. }
    102. // 创建一个线程任务来执行
    103. staticclassWorkerimplementsRunnable{
    104. @Override
    105. publicvoid run(){
    106. while(true){
    107. try{
    108. SelectionKey key = requestQueue.take();
    109. handleRequest(key);
    110. }catch(Exception e){
    111. e.printStackTrace();
    112. }
    113. }
    114. }
    115. privatevoid handleRequest(SelectionKey key)
    116. throwsIOException,ClosedChannelException{
    117. // 假设想象一下,后台有个线程池获取到了请求
    118. // 下面的代码,都是在后台线程池的工作线程里在处理和执行
    119. SocketChannel channel =null;
    120. try{
    121. // 如果说这个key是个acceptable,是个连接请求的话
    122. if(key.isAcceptable()){System.out.println("["+Thread.currentThread().getName()+"]接收到连接请求");
    123. ServerSocketChannel serverSocketChannel =(ServerSocketChannel) key.channel();
    124. // 调用accept方法 和客户端进行三次握手
    125. channel = serverSocketChannel.accept();System.out.println("["+Thread.currentThread().getName()+"]建立连接时获取到的channel="+ channel);
    126. // 如果三次握手成功了之后,就可以获取到一个建立好TCP连接的SocketChannel
    127. // 这个SocketChannel大概可以理解为,底层有一个Socket,是跟客户端进行连接的
    128. // 你的SocketChannel就是联通到那个Socket上去,负责进行网络数据的读写的
    129. // 设置为非阻塞的
    130. channel.configureBlocking(false);
    131. // 关注的是Reade请求
    132. channel.register(selector,SelectionKey.OP_READ);}
    133. // 如果说这个key是readable,是个发送了数据过来的话,此时需要读取客户端发送过来的数据
    134. elseif(key.isReadable()){
    135. channel =(SocketChannel) key.channel();
    136. ByteBuffer buffer =ByteBuffer.allocate(1024);
    137. int count = channel.read(buffer);
    138. // 通过底层的socket读取数据,写入buffer中,position可能就会变成21之类的
    139. // 你读取到了多少个字节,此时buffer的position就会变成多少
    140. System.out.println("["+Thread.currentThread().getName()+"]接收到请求");
    141. if(count >0){
    142. buffer.flip();// position = 0,limit = 21,仅仅读取buffer中,0~21这段刚刚写入进去的数据
    143. System.out.println("服务端接收请求:"+newString(buffer.array(),0, count));
    144. channel.register(selector,SelectionKey.OP_WRITE);
    145. }
    146. }elseif(key.isWritable()){
    147. ByteBuffer buffer =ByteBuffer.allocate(1024);
    148. buffer.put("收到".getBytes());
    149. buffer.flip();
    150. channel =(SocketChannel) key.channel();
    151. channel.write(buffer);
    152. channel.register(selector,SelectionKey.OP_READ);
    153. }
    154. }
    155. catch(Throwable t){
    156. t.printStackTrace();
    157. if(channel !=null){
    158. channel.close();
    159. }
    160. }
    161. }
    162. }
    163. }
    总结:
        通过本篇文章,主要是分析了常见的NIO的一些问题:
    • BIO, NIO, AIO各自的特点
    • 什么同步阻塞,同步非阻塞,异步非阻塞
    • 为什么NIO能够应对支持海量的请求
    • NIO相关组件的原理
    • NIO通讯的简单案例
    本文仅仅是介绍了一下网络通讯的一些原理,应对面试来讲解
     
    NIO通讯其实有很多的的东西,在中间件的研发过程中使用的频率还是非常高的,后续有机会再和大家分享交流。
    本站所有文章均由网友分享,仅用于参考学习用,请勿直接转载,如有侵权,请联系网站客服删除相关文章。若由于商用引起版权纠纷,一切责任均由使用者承担
    极客文库 » 10个最高频的Java NIO面试题剖析!

    常见问题FAQ

    如果资源链接失效了怎么办?
    本站用户分享的所有资源都有自动备份机制,如果资源链接失效,请联系本站客服QQ:2580505920更新资源地址。
    如果用户分享的资源与描述不符怎么办?
    可以联系客服QQ:2580505920,如果要求合理可以安排退款或者退赞助积分。
    如何分享个人资源获取赞助积分或其他奖励?
    本站用户可以分享自己的资源,但是必须保证资源没有侵权行为。点击个人中心,根据操作填写并上传即可。资源所获收益完全归属上传者,每周可申请提现一次。
    如果您发现了本资源有侵权行为怎么办?
    及时联系客服QQ:2580505920,核实予以删除。

    参与讨论

    • 211会员总数(位)
    • 3737资源总数(个)
    • 0本周发布(个)
    • 0 今日发布(个)
    • 869稳定运行(天)

    欢迎加入「极客文库」,成为原创作者从这里开始!

    立即加入 了解更多
    成为赞助用户享有更多特权立即升级