美文网首页Amazing Arch架构
快速掌握NIO(上)

快速掌握NIO(上)

作者: 叩丁狼教育 | 来源:发表于2018-10-08 11:07 被阅读209次

    本文作者:禹明明,叩丁狼高级讲师。原创文章,转载请注明出处。

    NIO概述

    NIO是JDK1.4引入的新的IO模型,是New I/O的简称,现在更多人认为应该是 Non-blocking(非阻塞) IO的简称,NIO提供了比传统IO更高的性能和更优的操作方式

    JDK1.4之前我们使用的IO是同步阻塞的,我们可以称之为BIO(阻塞IO)
    JDK1.4Java学习了Linux的select模式提供了新的同步非阻塞IO模式NIO(非阻塞IO)
    JDK1.7 的NIO2学习了Linux的epoll模式才是真正实现了(非阻塞异步IO),我们称之为AIO,但是由于AIO用的不多,我们就暂不讨论

    JAVA的IO模型提供了标准输入输出(操作文件) 和网络编程两套API。
    但是NIO对于标准输入输出的性能提升并没有那么明显(其实IO底层已经使用了NIO的技术重新实现过),但是对于网络编程方面,NIO对性能的提升是非常巨大的,目前非常流行Mina和Netty都是对NIO的一种封装

    NIO标准输入输出API

    普通的IO我们都非常熟练了,我们来看一个普通IO和NIO复制文件的代码对比

    
     private long testNIO()throws IOException{
            File src = new File("D:/src.txt");
            File dest = new File("D:/dest.txt");
            long startTime = System.currentTimeMillis();
    
            if(!dest.exists())
    
                dest.createNewFile();
    
            RandomAccessFile read =new RandomAccessFile(src,"rw");
    
            RandomAccessFile write =new RandomAccessFile(dest,"rw");
    
            FileChannel readChannel = read.getChannel();
    
            FileChannel writeChannel = write.getChannel();
    
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024*1024);//1M缓冲区
    
            while(readChannel.read(byteBuffer) >0) {
    
                byteBuffer.flip();//翻转状态,从写模式切换到读模式(必须!)
    
                writeChannel.write(byteBuffer);
    
                byteBuffer.clear();//重置Buffer位置,相当于清空Buffer,但是只是改变位置指向,不真正删除数据
    
            }
    
            writeChannel.close();
            readChannel.close();
            long endTime = System.currentTimeMillis();
    
            return endTime - startTime;
    
        }
    
        public void testIO(){
            File src = new File("D:/src.txt");
            File dest = new File("D:/dest.txt");
    
            try (
                    InputStream in = new FileInputStream(src);
                    OutputStream out = new FileOutputStream(dest);
            ) {
                byte[] buffer = new byte[1024];
                int len = -1;
                while ((len = in.read(buffer)) != -1) {
                    out.write(buffer, 0, len);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    

    这部分我们学习一下怎么使用就行,不做过多讨论。通常当我们提到NIO的时候更多关注的是网络通信部分

    网络编程API

    要了解NIO我们需要对比一下传统 BIO 网络通信模型和 NIO通信模型的区别

    先了解一下NIO的三个核心概念:

    • buffer缓冲区
    • Channel管道
    • Selector选择器

    缓冲区 Buffer

    传统IO是面向stream的,NIO是面向缓冲区(Buffer)的
    Buffer是一个对象,包含一些要写入或者读出的数据。
    在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的.在写入数据时也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

    具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。最常用的就是ByteBuffer, 他们实现了相同的接口:Buffer。

    Buffer中比较重要的4个属性:position、limit、capacity、mark. 在使用 Buffer 时,我们实际操作的就是这四个属性的值.
    具体介绍下4个属性:

    • capacity(容量):一个buffer能够容纳数据元素的最大数量,capacity不会为负数,且永远不能被改变.
      假设:IntBuffer.allocate(1024), 分配了大小为1024的元素个数,则capacity就等于1024.
    • limit(上界):一个buffer的limit指的是第一个不能在读也不能在写的元素索引. limit不会为负数,并且一定是小于capacity的.
      假设:IntBuffer.allocate(1024), 我们在程序中设置limit=512,说明Buffer的容量是1024,但是从512之后既不能读也不能写了,进一步说明该Buffer的实际可用大小是512.
    • position(位置):一个buffer的position指的是下一个将要读或者写的元素的索引.position不会为负数,并且一定是小于limit的. position的位置主要由get()和put()方法的调用来更新.
    • mark(标记):一个备忘地址,作为临时标记位置使用,标记在设定前是未定义的.
      mark的使用场景:
      假设IntBuffer.allocate(1024),现在position位置为10,现在只想发送512到1024之间的缓冲数据,此时我们可以buffer.mark(buffer.position())既将position记入mark位置,然后buffer.postion(512),此时发送的数据就是512到1024之间的数据。发送完成后,调用buffer.reset()将mark临时标记赋值给position使得position=mark。注意如果未设定mark,而调用了buffer.reset()方法则会抛出InvalidMarkException.
      不变式:
      0 <= mark <= position <= limit <= capacity
    image.png

    通道 Channel

    我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。

    底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。

    Channel主要分两大类:

    • SelectableChannel:用户网络读写
    • FileChannel:用于文件操作

    多路复用器 Selector

    Selector是Java NIO 编程的基础。
    Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

    一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

    • 下面是普通的BIO通信模型


      image.png

      采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。Blocking-IO是典型的一请求一应答的模型,每一个Socket的处理过程当中都是阻塞的。
      BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程来处理这条链路,在需要满足高性能、高并发的场景是没法应用的(大量创建新的线程会严重影响服务器性能,甚至罢工)。

    再来对比一下NIO的模型


    image.png

    阻塞IO的问题主要体现在三个方面:

    1. 创建大量线程,浪费内存
    2. 由于是阻塞模式,线程在等待任务处理完成的时间都是阻塞的,没有任何意义
    3. 线程太多,就需要CPU在多个线程之间进行切换,浪费大量CPU时间

    而NIO的流程并没有为每一个client都去创建一个线程,而是使用了一个Selector来轮询已经准备就绪的key(先简单理解为一个key对应一个Channel),这样就可以节省大量的线程和线程切换的开销而且不会对性能造成太大影响,尤其在多线程高并发的时候,NIO的性能也不会像IO一样出现急剧下降甚至宕机。

    拿一个买票例子来说IO和NIO的区别就是:
    IO就相当于每个人(线程)都排几百米的队,自己去买票,而卖票(任务处理)的速度是一定的,排队期间你(线程)做不了任何事情(阻塞),这样不但无法提高卖票效率,反而容易造成大量拥堵
    NIO就是找黄牛买票,黄牛买到了打电话通知你,而这个黄牛(Selector)同时在为成千上万个需要买票的人(Channel)服务,哪个票能买了(就绪),黄牛马上能够知道,然后就去找到对应的客户(Chanel)去处理

    但是NIO缺点也不是没有,那就是API比较复杂,学习成本较高,不好维护。所以如果是并发量不高的简单服务最好还是使用传统IO,方便维护。而对于高并发的系统最好采用NIO,但是一般也不会直接使用NIO的原生API,而是使用NIO框架Mina或者Netty

    想获取更多技术视频,请前往叩丁狼官网:http://www.wolfcode.cn/openClassWeb_listDetail.html

    相关文章

      网友评论

        本文标题:快速掌握NIO(上)

        本文链接:https://www.haomeiwen.com/subject/kgmdaftx.html