BIO、NIO的整体理解

BIO、NIO的整体理解

一、考点分析

1、考察点

  • 基础 API 功能与设计,InputStream/OutputStream 和 Reader/Writer 的关系和区别

  • NIO、NIO2的基本组成.

  • 给定场景,分别用不同模型实现,分析BIO, NIO等模式的设计和实现原理.

  • NIO提供的高性能数据操作方式是基于什么原理。如何使用?

  • 或者,从开发者的角度来看,你觉得NIO自身实
现存在哪些问题?有什么改进的想法吗?

2、java.io 必备知识

  • IO不仅仅是对文件的操作,网络编程中,比如Socket通信, 都是典型的IO操作目标。

  • 输入流,输出流(Inputstream/Outputstream)是用于读取或写入字节的,例如操作图片
文件。

  • Reader/Writer则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取
或者写入文本信息本质上计算机操作的都是字节,不管是网络通信还是文件读取,
Reader/Writer相当于构建了应用逻辑原始数据

之间的桥梁

  • BufferedOutputStream等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高IO处理
效率。这种设计利用了缓冲区,将批量数据进行一次操作,但在使用中千万别忘了 flush.

  • 参考下面这张类图。很多IO工具类都实现了 Closeable接口,因为需要迸行资源的释放。
比如,打开Fileinputstream ,它就会获取相应的文件描述符(FileDescriptor),需要利用
try-with-resourcestry-finally等机制保证FilelnputStream被明确关闭,进而相应文件
描述符也会失效,否则将导致资源无法被释放。利用Cleaner或
finalize机制作为资源释放的最后把关,也是必要的。

在这里插入图片描述

二、知识拓展

1、基础概念

同步和异步,阻塞和非阻塞
  • 区分同步和异步( synchronous/asynchronous)。简单来说,同步是一种可靠的有序运行
机制;而异步
则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制实现任务间次序关系
  • 区分阻塞与非阻塞(blocking/non-blocking )。在进行阻塞操作时,当前线程会处于阻塞
状态,无法从事其他任务,只有当条件就绪才能继续,比如Serversocket新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理。
  • 不能一概而论认为同步或阻塞就是低效,具体还要着应用和系统特征。

2、BIO(传统IO)

  • 首先,传统的 java.io包,它基于流模型实现,提供了我们最熟知的一些IO功能,比如File抽
象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流
时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
  • java.io包的好处是代码比较简单、直观,缺点则是IO效率和扩展性存在局限性,容易成为应用性能的瓶颈。
  • 很多时候,人们也把java.net下面提供的部分网络API,比如Socket, ServerSocket,HttpURLConnection也归类到同步阻塞IO类库,因为网络通信同样是IO行为.
  • 请跳转到这里 IO详解之BIO

3、NIO

1)NIO概览
  • 在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、 Buffer 等
新的抽象,可以构建多路复用的、同步非阻塞IO程序,同时提供了更接近操作系统底层的高性能数据操作方式
    在这里插入图片描述

  • NIO的主要组成部分:

    • Buffer:高效的数据容器,除了布尔类型,所有原始数据类型都有相应的Buffer实现

    • Channel:类似在Linux之类操作系统上看到的文件描述符,是NIO中被用来支持批量式IO操作的一种抽象。

    • File成者Socket:通常被认为是比较高层次的抽象。而Channel则是更加操作系统底层的
一种抽象,这也使得NIO得以充分利用现代操作系统底层机制,获得特定场景的性能优化,,例如,DMA ( Direct Memory Access)等。不同层次的抽象是相互关联的,我们可以通过
Socket获取Channel,反之亦然,

    • Selector: 是NIO实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在
Selector上的多个Channel中,是否有Channel处于就绪状态 ,进而实现了单线程对多
Channel的高效管理

      Selector同样是基于底层操作系统机制,不同模式,不同版本都存在区別,例如,在最新的
代码库里,相关实现如下:

      • Linux上依赖于
epoll (http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/iava.base/linux/classes/sun/nio/ch/EPollSelectorlmpl.java)

      • Windows 上 NIO2 ( AIO)模式依赖于

        iocp ( http://hg.openjdk.java.net/jdk/jdk/fiIe/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/locp.java).

    • Charset:提供Unicode 字符串定义,NlO 也提供了相应的编解码器等,例如通过下面的方式进行字符串到ByteBuffer的转换

      Charset.defaultCharset().encode("Hello world!")
2)NIO可解决什么问题
场景描述:
  • 我们需要实
现一个服务器应用,只简单要求能够同时服务多个客户端请求即可。
方法1:BIO实现
  • 使用java.io和java.net中的同步、阻塞式API,可以简单实现.

    public class DemoServer extends Thread{
    
        //服务端线程
        private ServerSocket serverSocket;
    
        public int getPort(){
            return serverSocket.getLocalPort();
        }
    
        @Override
        public void run() {
            try{
                //设置端口
                serverSocket = new ServerSocket(0);
                while(true){
                    //调用accept方法,阻塞等待客户端连接.
                    Socket socket = serverSocket.accept();
                    RequestHandler requestHandler = new RequestHandler(socket);
                    requestHandler.start();
                }
    
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) throws Exception {
            DemoServer demoServer = new DemoServer();
            demoServer.start();
            for (int i = 0; i < 6 ; i++) {
                //客户端线程:主机地址,端口号连接服务端
                try(Socket client = new Socket(InetAddress.getLocalHost(),demoServer.getPort())){
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
                    bufferedReader.lines().forEach(s->{
                        System.out.println(s);
                    });
                }
            }
        }
    }
    
    //简化实现,不做读取,直接发送字符串
    class RequestHandler extends Thread{
        private Socket socket;
        RequestHandler(Socket socket){
            this.socket = socket;
        }
    
        @Override
        public void run() {
            try(PrintWriter out = new PrintWriter(socket.getOutputStream());){
                out.println("hello world");
                out.flush();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
    输出结果
    ---------
    hello world
    hello world
    hello world
    hello world
    hello world
    hello world
    (服务器继续阻塞等待socket客户端发来请求)
    
  • 其实现要点是:

    • 服务端启动ServerSocket,端口 0表示自动绑定一个空闲端口
    • 调用accept方法,阻塞等待客户端连接.
    • 利用Socket模拟了一个简单的客户端,只进行连接、读取、打印。
    • 当连接建立后,**启动一个单独线程负责回复(RequstHandler)**客户端请求.
  • 出现的问题:

    • Java语言目前的线程实现是比较重置级的,启动或者销毁一个线程有明显开销的,
每个线程都有单独的线程栈等结构,需要占用非常明显的内存,所以,每一个Client启动一个
线程似乎都有些浪费

方法2:关于BIO线程池机制
  • 引入线程池机制的BIO避免线程销毁,减少重新创建启动线程,销毁线程带来的浪费

    ExecutorService executors = Executors.newFixedThreadPool(8);
    
     //利用线程池机制,为requestHandler分配线程,来处理client请求
    while(true){
      Socket socket = serverSocket.accept();
      RequestHandler requestHandler = new RequestHandler(socket);
      //requestHandler.start();
      executors.execute(requestHandler);
    }
    
  • 实现要点:

    通过一个固定大小的线程池,来负责管理工作线程,避免频繁创建,销毁线程的开销,这是我们构建并发服务的典型方式。这种工作方式,可以参考下图来理解。

在这里插入图片描述

  • 存在的问题:

    如果连接数并不是非常多,只有最多几百个连接的普通应用,这种模式往往可以工作的很好。但
是,如果连接数量急剧上升,这种实现方式就无法很好地工作了,因为线程上下文切换开销会在
高并发时变得很明显,这是同步阻塞方式的低扩展性劣势

方法3:NIO的多路复用机制
public class NIOServer extends Thread{

  @Override
  public void run() {

    try(Selector selector = Selector.open()){

      //创建server的channel
      ServerSocketChannel serverSocket = ServerSocketChannel.open();
      serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(),8888));
      //设置server的channel非阻塞。阻塞模式下,注册操作是不允许的
      serverSocket.configureBlocking(false);

      //将serverChannel注册到selector,并说明关注点
      serverSocket.register(selector, SelectionKey.OP_ACCEPT);

      //轮询访问selector
      while (true){
        //阻塞并等待就绪的socket Channel,这是关键点之一
        selector.select();
        //在这段时间内,得到所有访问selector的socket channel(socketChannel(多个)和serverSocketChannel(一个)是一对)
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while(iterator.hasNext()){
          SelectionKey next = iterator.next();
          sayHelloWorld((ServerSocketChannel)next.channel());
          iterator.remove();
        }
      }

    } catch (IOException e) {
      e.printStackTrace();
    }

  }

  //client利用socket高层抽象对服务器发出请求,服务器用底层的抽象socketChannel,selector进行处理
  private  void sayHelloWorld(ServerSocketChannel serverSocketChannel){
    //
    try(SocketChannel socketChannel = serverSocketChannel.accept()) {
      //处理socketChannel:服务器端将字符串写入byteBuff
      socketChannel.write(Charset.defaultCharset().encode("hello wangxiaoxi"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) throws Exception{
    NIOServer nioServer = new NIOServer();
    nioServer.start();

    for (int i = 0; i < 6; i++) {
      try(Socket socket = new Socket(InetAddress.getLocalHost(),8888)){

        //服务端已将字符串写入byteBuff,客户端读取byteBuff里的数据
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        bufferedReader.lines().forEach(s->{
          System.out.println(s);
        });
      }
    }
  }
}

输出结果
-------------
hello wangxiaoxi
hello wangxiaoxi
hello wangxiaoxi
hello wangxiaoxi
hello wangxiaoxi
hello wangxiaoxi

  • 首先,通过Selector.open()创建一个Selector,作为类似调度员的角色.

  • 然后,创建一个ServerSocketChannel,并且向Selector注册,通过指定
SelectionKey.OP_ACCEPT ,告诉调度员,它关注的是新的连接请求.

    注意,为什么我们要明确配宜非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常.

  • Selector阻塞在select操作,当有Channel 发送接入请求,就会被唤醒.

  • 在sayHelloWorld方法中,通过Socketchannel和Buffer进行数据操作,在本例中是发送
了一段字符串。

  • NIOServer 和 BIOServer的main方法类似

备注:

  • 基本抽象很相似:SocketChannel(N) 和ServerSocketChannel(1)是一对 ,他们是java.nio下面实现通信的类,支持异步通信。就好比java.net下,Socket 和ServerSocket 是一对。
  • 什么是多路复用:很多个网络I/O复用一个或少量的线程来处理这些连接。
3)NIO与BIO的区别
  • 可以看到,在前面两个样例中,IO都是同步阻塞模式,所以需要多线程以实现多任务处理。而
NIO则是利用了单线程轮询事件的机制(事件驱动),通过高效地定位就绪的Channel ,来决定做什么,仅
仅select阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的
扩展能力有了非常大提高。下面这张图对这种实现思路进行了形象地说明。
    在这里插入图片描述
4)Netty与Java的NIO的区别

4、NIO2(AIO)

  • Java 7中,NIO有了进一步改进,也就是NIO 2,引入了异步非阻塞方式,也
有很多人叫它AIO (Asynchronous IO)。异步IO操作基于事件和回调的机制,可以简
单理解为,应用操作直接返回,而不会阻塞那里,当后台处理完成,操作系统会通知相应线程进行后续工作。
  • NIO2利用事件和回调,处理
Accept、Read等操作。AIO实现看起来是类似这样子:
  • NIO和NIO2的区别:
    • 基本抽象很相似:AsynchronousServerSocketChannel对应于上面例子中的
ServerSocketChannel; AsynchronousSocketChannel 则对应 Socketchannel。
    • 业务逻辑的关键在于,通过指定CompletionHandler回调接口,在accept/read/write等
关键节点,通过事件机制调用,这是非常不同的一种编程思路。

5、小总结

​ 比较了BIO,NIO,NIO2的设计思路,在我看来,以烤串店为例(假设只有一个卖串口。在大东北有些热闹片区,外面就有这样的小店)

  • BIO: 烤肉店老板雇了很多员工,一个员工为一个顾客拿串串(那么小一个地方,哪能容得下那么多员工,效率太低了)
  • NIO: 烤肉店老板解雇了所有的员工,夫妻两人开店,丈夫烤肉,妻子到外面卖肉(减少了大量的人工成本,效率提高了不少)
  • NIO2: 自从有了美团,烤肉店老板常常接到网上订购烤串客户的电话,生意又好了不少。

所以要结合具体场景,选择是使用BIO,NIO还是NIO2,使系统效能达到更高,不能一概而论,但在理论上,对于高并发场景,NIO2效率更高。

三、参考文档

1、极客时间《Java核心技术36讲》第11讲

2、 【java】IO详解之BIO

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值