前言:IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。现在使用NIO的场景越来越多,很多网上的技术框架或多或少用到了NIO技术,譬如Tomcat、Jetty,还有基于nio的网络编程框架Netty。


一、什么是NIO?

1.1、高并发量引起的问题

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

1、线程不够用, 就算使用了线程池复用线程也无济于事;

2、阻塞I/O模式下,会有大量的线程被阻塞,一直在等待数据,这个时候的线程被挂起,只能干等,CPU利用率很低,换句话说,系统的吞吐量差;

3、如果网络I/O堵塞或者有网络抖动或者网络故障等,线程的阻塞时间可能很长。整个系统也变的不可靠;

1.2、NIO简介

Java NIO 全称 java non-blocking IO,是指JDK 1.4 及以上版本提供的新API(New IO)。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供同步非阻塞式的高伸缩性网络。

NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。

NIO 有三大核心部分:Channel(通道) , Buffer(缓冲区) ,Selector(选择器) 。

  • NIO 是 面向缓冲区 ,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络

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

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

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

I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流。发明IO多路复用的原因,是尽量提高服务器的吞吐能力。

1.3、NIO 和 BIO 的比较

  • BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。

  • BIO 是阻塞的,NIO 则是非阻塞的。

  • BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道 。

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

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

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

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

阻塞I/O模型

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

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

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

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

1.6、 NIO 三大核心原理示意图

一张图描述 NIO 的 Selector 、 Channel 和 Buffer 的关系

  • 每个channel 都会对应一个Buffer。

  • Selector 对应一个线程, 一个线程对应多个channel(连接)。

  • 该图反应了有三个channel 注册到 该selector

  • 程序切换到哪个channel 是由事件决定的, Event 就是一个重要的概念

  • Selector 会根据不同的事件,在各个通道上切换

  • Buffer 就是一个内存块 , 底层是有一个数组

  • 数据的读取写入是通过Buffer, 这个和BIO不同 , BIO 中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer 是可以读也可以写, 需要 flip 方法切换

  • channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统通道就是双向的.


二、NIO的核心实现

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

NIO核心API Channel, Buffer, Selector

2.1、通道Channel

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

1. 通道可以同时进行读写,而流只能读或者只能写

2. 通道可以实现异步读写数据

3. 通道可以从缓冲读数据,也可以写数据到缓冲:

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

2.2、缓存Buffer

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 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在读写模式中的说明:

buffer中三个关键属性capacity,position以及limit在读写模式中的说明
 

缓冲区常用的操作

向缓冲区写数据:

1. 从Channel写到Buffer;

2. 通过Buffer的put方法写到Buffer中;

从缓冲区读取数据:

1. 从Buffer中读取数据到Channel;

2. 通过Buffer的get方法从Buffer中读取数据;

flip方法:

将Buffer从写模式切换到读模式,将position值重置为0,limit的值设置为之前position的值;

clear方法 vs compact方法:

clear方法清空缓冲区;compact方法只会清空已读取的数据,而还未读取的数据继续保存在Buffer中;

2.3、Selector

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

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)。

  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之间的上下文切换导致的开销。

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

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

三、nio、bio和aio的区别

BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。

NIO (New I/O):同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。

AIO ( Asynchronous I/O):异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。

进程中的IO调用步骤大致可以分为以下四步:

进程向操作系统请求数据 ;

操作系统把外部数据加载到内核的缓冲区中;

操作系统把内核的缓冲区拷贝到进程的缓冲区 ;

进程获得数据完成自己的功能 ;

当操作系统在把外部数据放到进程缓冲区的这段时间(即上述的第二,三步),如果应用进程是挂起等待的,那么就是同步IO,反之,就是异步IO,也就是AIO 。

BIO、NIO、AIO适用场景分析:

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。


四、Netty是什么?

netty 是一个基于nio的客户、服务器端编程框架,netty提供异步的,事件驱动的网络应用程序框架和工具,可以快速开发高可用的客户端和服务器。netty是基于nio的,它封装了jdk的nio,让我们使用起来更加方便灵活。Dubbo底层就是用Netty实现的。

netty是由jboss提供的一款开源框架,常用于搭建RPC中的TCP服务器、WebSocket服务器,甚至是类似Tomcat的web服务器,反正就是各种网络服务器,在处理高并发的项目中,有奇用!功能丰富且性能良好,基于Java中NIO的二次封装,具有比原生NIO更好更稳健的体验。

官网给出的Netty底层示意图:Netty用到了零拷贝技术

NIO的优势:

传统IO中,每创建一个连接都要创建一个线程来维护,极大地浪费资源和增加服务器的压力,传统IO线程切换效率低下,而且传统IO是面向字节流的,一次性只能从流中读取一个或者多个字节,并且读完之后流无法再读取,你需要自己缓存数据。效率不高。

NIO模型中,每个新连接进来,不是创建一个新线程,而是将连接绑定到一个selector中,然后这条连接的读写都由这个selector来管理,因为每个线程都会管理着一批连接,充分利用了资源和提高了效率,NIO是面向buffer的,可以随意读取里面任何一个字节数据,不需要你自己缓存数据,这一切只需要移动读写指针即可。


五、常见的IO多路复用模型:select,poll,epoll

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

(1) select==>时间复杂度O(n)

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

(2) poll==>时间复杂度O(n)

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的替换原有fd_set数据结构

(3) epoll==>时间复杂度O(1)

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))


参考链接:

真正理解NIO

什么还不懂Java NIO,看这一篇博客就够了!

Java中nio、bio和aio的区别相关推荐

  1. Java中实现接口与继承的区别

    ** Java中实现接口与继承的区别 ** 首先,先来了解一下什么是接口和继承.接口一般是使用interface来定义的.接口定义同类的定义类似,分为接口的声明和接口体,其中接口体由常量定义和方法定义 ...

  2. Java中PreparedStatement和Statement的用法区别

    Java中PreparedStatement和Statement的用法区别 (2012-08-01 11:06:44) 转载▼ 标签: 杂谈   1. PreparedStatement接口继承Sta ...

  3. Java中long和Long有什么区别

    Java中long和Long有什么区别(转) Java的数据类型分两种: 1.基本类型:long,int,byte,float,double,char 2. 对象类型(类): Long,Integer ...

  4. java中的sleep()和wait()的区别

    对于sleep()方法,我们首先要知道该方法是属于Thread类中的.而wait()方法,则是属于Object类中的. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监 ...

  5. Java中活锁和死锁有什么区别?

    Java中活锁和死锁有什么区别? 活锁:一个线程通常会有会响应其他线程的活动.如果其他线程也会响应另一个线程的活动,那么就有可能发生活锁.同死锁一样,发生活锁的线程无法继续执行.然而线程并没有阻塞-- ...

  6. Java中wait和sleep方法的区别

    1.两者的区别 这两个方法来自不同的类分别是Thread和Object 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁). wait ...

  7. java comparator相等_详解Java中Comparable和Comparator接口的区别

    详解Java中Comparable和Comparator接口的区别 发布于 2020-7-20| 复制链接 摘记: 详解Java中Comparable和Comparator接口的区别本文要来详细分析一 ...

  8. java == hashcode,java中==和equals和hashCode的区别

    java中==和equals和hashCode的区别 == 的作用: 基本类型:比较的就是值是否相同 引用类型:比较的就是地址值是否相同(确切的说,是堆内存地址) equals 的作用: 引用类型:默 ...

  9. java system sleep_详解Java中的sleep()和wait()的区别

    详解Java中的sleep()和wait()的区别 对于sleep()方法,我们首先要知道该方法是属于Thread类中的.而wait()方法,则是属于Object类中的. sleep()方法导致了程序 ...

最新文章

  1. 《Ossim应用指南》入门篇
  2. 在android上移植ios
  3. c多线程并发处理方式_ElasticSearch 并发的处理方式:锁和版本控制
  4. linux ubuntu 17,Ubuntu 17.04(Zesty Zapus)已经结束支持,请升级到Ubuntu 17.10
  5. Android之webview与js交互
  6. php 修改 wordpress,wordpress怎么编辑代码修改页面
  7. extend_gcd求解不定方程/膜线性方程/乘法(模)逆元
  8. 阿里云镜像下载ubuntu
  9. 红橙Darren视频笔记 从AIDL Demo分析Android源码走向
  10. mos 控制交流_小米智能排插的220V交流电压信号的开关控制电路设计
  11. Windows RPC Demo实现
  12. UnauthorizedAccessException Invaild cross-thread access
  13. Failed to connect to Etherscan API at url https://api-rinkeby.etherscan.io/api
  14. 客群洞察-大数据技术驱动互联网金融进阶之路
  15. 为了背单词,我花了两天写了一款背单词小程序
  16. Oracle Spatial详解
  17. java基础开发试题_Java应用开发基础-中国大学mooc-试题题目及答案
  18. 一文教你如何挑选深度学习GPU
  19. MONKOVEL-安卓端免费的小说阅读器 源码分享
  20. 不知道怎么识别手写文字?快来看这些手写文字识别成文档软件

热门文章

  1. AI换脸踩坑记录:Faceshifter
  2. SteamVR 2.x 手柄射线与3D物体交互(9)
  3. 当n趋向于无限大时,n的0.1次方和logn谁大
  4. Save Failed Compilation unit name must end with .java, or one of the registered java-like extensions
  5. 冲激序列的傅立叶变换
  6. c语言黄金分割法求最小值,黄金分割法求极值(c编程)
  7. Excel如何进行条件计数
  8. 在c语言中3ap是不合法的变量,C语言陷阱一
  9. 致生活中那些疲惫的人
  10. 自动化测试+性能面试题整理