真正理解NIO

目录

前言

什么是NIO

IO和NIO的区别

NIO的核心实现

通道Channel

缓存Buffer

缓冲区常用的操作

Selector

参考文章


前言

高并发量引起的问题。

一个使用传统阻塞I/O的系统,如果还是使用传统的一个请求对应一个线程这种模式,一旦有高并发的大量请求,就会有如下问题: 

  1. 线程不够用, 就算使用了线程池复用线程也无济于事; 
  2. 阻塞I/O模式下,会有大量的线程被阻塞,一直在等待数据,这个时候的线程被挂起,只能干等,CPU利用率很低,换句话说,系统的吞吐量差; 
  3. 如果网络I/O堵塞或者有网络抖动或者网络故障等,线程的阻塞时间可能很长。整个系统也变的不可靠;

什么是NIO

java.nio全称java non-blocking IO(实际上是 new io),是指JDK 1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。

HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

下面是从知乎 https://zhuanlan.zhihu.com/p/62260286  抄过来的一段话。

要搞清楚什么是NIO,要先搞清楚什么是BIO,即阻塞IO,看一段代码:

        ServerSocket serverSocket = new ServerSocket(port);
        OutputStream os = null;
        InputStream is = null;

        try {
            Socket socket = serverSocket.accept();
            is = socket.getInputStream();
            byte[] b = new byte[1024];
            int n = is.read(b);
            os = socket.getOutputStream();
            os.write(b, 0, n);
        }catch (Exception e){
            
        }finally {
            is.close();
            os.close();
        }

上面的代码中有两处阻塞的地方,一个是accept函数会调用到一个native方法accept0(nativefd, isaa);直到建立新的tcp连接。另外一处是read函数在没有消息的时候会一直阻塞,直到接收到新的消息。java在1.4以前没有NIO的时候处理网络消息的办法就只能是每次有一个新的连接,开启一个新的线程,或者从线程池中取出一个线程,这个线程执行的逻辑是使用一个while循环来不断接收消息,接收到消息以后处理消息或者加入消息队列交给其他线程处理。即:

            new Thread(()->{
                while (true){
                    is = socket.getInputStream();
                    byte[] b = new byte[1024];
                    int len = is.read(b);
                    //向客户端发送反馈内容
                    os = socket.getOutputStream();
                    os.write(b, 0, len);
                }
            });

现在来到了我们的NIO,NIO在Linux上使用了epoll这个系统调用,epoll能够做到已注册的连接在消息到来的时候主动通知调度器,将消息加入消息队列。这个调度器就是java NIO的Selector,Selector的select设定一个超时时间,获取消息队列中的消息然后分发给工作线程异步进行解码等操作。

NIO将上面的代码分给了至少三个线程去完成,一个处理连接的线程,一个消息调度线程,以及至少一个工作线程。NIO降低了任务的粒度并且避免阻塞io中大量的线程阻塞占用过多的内存。实际代码中,工作线程们不能直接处理占用过多cpu时间的操作,应当将处理好的数据放入消息队列或者直接交给异步线程池来处理,否则会阻塞后续消息的接收,又回到了阻塞io的问题上。

NIO,即非阻塞io最重要的其实就是解决了read函数的阻塞问题,然后分离读消息与处理消息(类似与业务中专门使用sql处理线程池异步处理sql相关的处理)减少大量的线程占用。

IO和NIO的区别

原有的 IO 是面向流的、阻塞的,NIO 则是面向块的、非阻塞的。

怎么理解IO是面向流的、阻塞的?

java1.4以前的io模型,一连接对一个线程。

原始的IO是面向流的,不存在缓存的概念。Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区

Java IO的各种流是阻塞的,这意味着当一个线程调用read或 write方法时,该线程被阻塞,直到有一些数据被读取,或数据完全写入,该线程在此期间不能再干任何事情了。

怎么理解NIO是面向块的、非阻塞的?

NIO是面向缓冲区的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性。

Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。

NIO的核心实现

在标准IO API中,你可以操作字节流和字符流,但在新IO中,你可以操作通道和缓冲,数据总是从通道被读取到缓冲中或者从缓冲写入到通道中。

NIO核心API Channel, Buffer, Selector 。

通道Channel

NIO的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写;
  • 通道可以实现异步读写数据;
  • 通道可以从缓冲读数据,也可以写数据到缓冲:;

可以从通道读取数据到缓冲区,也可以把缓冲区的数据写到通道中。

缓存Buffer

缓冲区本质上是一个可以写入数据的内存块,然后可以再次读取,该对象提供了一组方法,可以更轻松地使用内存块,使用缓冲区读取和写入数据通常遵循以下四个步骤:

  1. 写数据到缓冲区;
  2. 调用buffer.flip()方法;
  3. 从缓冲区中读取数据;
  4. 调用buffer.clear()或buffer.compat()方法;

当向buffer写入数据时,buffer会记录下写了多少数据,一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式,在读模式下可以读取之前写入到buffer的所有数据,一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。

Buffer在与Channel交互时,需要一些标志:

buffer的大小/容量 - Capacity

作为一个内存块,Buffer有一个固定的大小值,用参数capacity表示。

当前读/写的位置 - Position​

当写数据到缓冲时,position表示当前待写入的位置,position最大可为capacity – 1;当从缓冲读取数据时,position表示从当前位置读取。

信息末尾的位置 - limit

在写模式下,缓冲区的limit表示你最多能往Buffer里写多少数据; 写模式下,limit等于Buffer的capacity,意味着你还能从缓冲区获取多少数据。

下图展示了buffer中三个关键属性capacity,position以及limit在读写模式中的说明:

缓冲区常用的操作

向缓冲区写数据:

  • 从Channel写到Buffer;
  • 通过Buffer的put方法写到Buffer中;
int bytesRead = inChannel.read(buf); //read into buffer.
buf.put(127);

从缓冲区读取数据:

  • 从Buffer中读取数据到Channel;
  • 通过Buffer的get方法从Buffer中读取数据;
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();

flip方法:

flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。

rewind()方法:

Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。

clear方法 vs compact方法:

clear()方法会清空整个缓冲区。

compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

mark()与reset()方法:

通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:

buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset();  //set position back to mark.

 Scatter/Gather :

分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。

聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

// channel.read(bufferArray);
// channel.write(bufferArray);

Selector

一个Selector组件,可以监测多个NIO channel,看看读或者写事件是否就绪。多个NIO channel 以事件的方式注册到同一个Selector,从而达到用一个线程处理多个请求成为可能。

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。

一个thread对应多个channel,一个channel处理一个请求。

当你调用Selector的select()或者 selectNow() 方法它只会返回有数据读取的SelectableChannel的实例。

参考文章

学习NIO

美团wiki-NIO

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值