Java NIO是在jdk1.4开始使用的,它既可以说成“新I/O”,也可以说成非阻塞式I/O。
阻塞:暂停一个线程执行以等待某个结果的发生。
- 如果客户端还没有对服务器端发出请求,那么ServerSocket.accept()将会阻塞。
- 如果连接成功,当数据还没有准备好时对InputStream.read()的调用同样会阻塞。
当要处理多个连接时,需要采用多线程的方式,由于每个线程都拥有自己的栈空间,而且由于阻塞会导致大量的线程进行上下文切换,使得程序的运行效率非常低下。
1.相关概念
一个完整的IO操作包括两个阶段:
- 准备阶段(查看数据是否就绪):在执行I/O操作的时候需要等待I/O是否就绪,因为此刻IO设备在忙状态。
- 用户进程空间和内核空间的数据拷贝(内核将数据拷贝到用户线程) : 当等到数据准备好了,kernel就会将数据从从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
1.1 阻塞IO、非阻塞IO、同步IO、异步IO
根据IO对kernel IO操作两个阶段的感知能力可以分为一下四种IO模型:
- 对第一阶段:阻塞和非阻塞
- 对第二阶段:同步和异步
阻塞IO | 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到 有东西可读或者可写为止。比如去银行取钱,发现没有开门就会一直在那等着直到银行开门。 |
非阻塞IO | 非阻塞状态下, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待。比如柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去。 |
同步IO | 用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪。比如自己亲自去银行取钱。 |
异步IO | 用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)。比如告诉朋友银行卡密码,让他拿着银行卡去帮自己取钱。然后自己可以去干别的事。(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS) |
- 异步IO的性能较差,所以在安全性要求较高时的场景中才使用;
- 同步是指函数完成之前会一直等待;阻塞是指系统调用的时候进程会被设置为Sleep状态直到等待的事件发生(比如有新的数据)。
1.1.1阻塞(blocking IO)和非阻塞(non-blocking IO)
调用机制,用来描述进程处理调用的方式。
在IO中两者的区别主要体现在I/O未准备好时(IO操作第一阶段),用户线程是否可以做其他事情。
- 如果数据没有就绪,阻塞IO在查看数据是否就绪的过程中是一直等待,当前线程会被挂起,一直处于等待消息通知,不能执行其他业务;
- 如果数据没有就绪,非阻塞IO直接返回一个标志信息。
非阻塞过程会让出CPU,阻塞过程不会让出CPU(采用阻塞的方式,只有一个线程,采用轮询的方式一直对某个请求进行处理,完全不需要对线程进行切换)
1.1.2 同步和异步
通信机制,用来描述进程处理调用的方式。
关注的是IO操作结果的获知方式,主要区别在于IO结果未返回时(IO操作第二阶段)用户线程是否可以做其他事情。
- 同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程,调用方需要保持等待直到IO操作完成,进而通过返回获得结果。
- 异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。调用方在IO操作的执行过程中不需要保持等待,而是在操作完成后被动的接受(通过消息或回调)被调用方推送的结果。也就是说在异步IO中,不会对用户线程产生任何阻塞。
1.2 Java BIO、NIO、AIO
类型 | 含义 | 使用场景 |
---|---|---|
同步阻塞IO(JAVA BIO) | 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 | BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。 |
同步非阻塞IO(Java NIO) | 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问 | NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。 |
异步非阻塞IO (Java AIO(NIO.2) | 一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理。线程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。 | AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。 |
1.2.1 Java BIO
在JDK 1.4推出Java NIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO)
这种一请求一应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈。
- 当并发访问量增大、响应时间延迟增大之后,采用Java BIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和低时延
- 极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,
- 这会导致恶性循环,传统采用BIO的Java Web服务器如下所示(典型的如Tomcat的BIO模式):
采用该线程模型的服务器调度特点如下:
- 服务端监听线程Acceptor负责客户端连接的接入,每当有新的客户端接入,就会创建一个新的I/O线程负责处理Socket(一连接一线程)
- 客户端请求消息的读取和应答的发送,都由I/O线程负责
- 除了I/O读写操作,默认情况下业务的逻辑处理,例如DB操作等,也都在I/O线程处理
- I/O操作采用同步阻塞操作,读写没有完成,I/O线程会同步阻塞
BIO线程模型主要存在如下三个问题:
- 性能问题:一连接一线程模型导致服务端的并发接入数和系统吞吐量受到极大限制
- 可靠性问题:由于I/O操作采用同步阻塞模式,当网络拥塞或者通信对端处理缓慢会导致I/O线程被挂住,阻塞时间无法预测
- 可维护性问题:I/O线程数无法有效控制、资源无法有效共享(多线程并发问题),系统可维护性差
2. NIO实现原理
- 由一个专门的线程来处理所有的 IO 事件,并负责分发。
- 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
- 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的进程切换。
通过Selector、Channel和Buffer来实现NIO。
2.1 Channel
双向的非阻塞通道,在通道的两边都可以进行数据读写操作;
2.2 Selector
Selector可以同时监控多个Selectable Channel的IO状况,是异步IO的核心
服务端和客户端各自维护一个管理通道的对象,即Selector。该对象能检测一个或多个通道 (channel) 上的事件。
- 以服务端为例,如果服务端的通道上注册了读事件,某时刻客户端给服务端发送了一些数据.
- 阻塞I/O这时会调用read()方法阻塞地读取数据.
- NIO的服务端会在selector中添加一个读事件
- 服务端的处理线程会轮询地访问selector
- 如果访问selector时发现有感兴趣的事件到达,则处理这些事件
- 如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。
采用可复用和解复用的方式使得一个线程可以管理多个通道,可以把多个流合并成一个流,或者把一个流拆分成多个流的方式),类似于一个观察者。
2.3Key(由SelectionKey类表示)
封装一个特定Channel 1和一个特定的Selector之间的关系
2.4Buffer
用来保存数据,可以用来存放从Channel 1读取的数据,也可存放使用Channel 1进行发送的数据。
- 分类;ByteBuffer、CharBuffer等。
- 简化了开发人员对流数据的管理。
2.5实现:
1、需要处理的Channel的IO事件(例如,connect、read或write等)注册给Selector。channel可以注册到一个或多个Selector上以进行异步IO操作。
- 注册我们感兴趣的事件。一共有以下四种事件:
-
事件名 对应值 服务端接收客户端连接事件 SelectionKey.OP_ACCEPT(16) 客户端连接服务端事件 SelectionKey.OP_CONNECT(8) 读事件 SelectionKey.OP_READ(1) 写事件 SelectionKey.OP_WRITE(4) - 方法
-
channel.register(selector, SelectionKey.OP_ACCEPT); channel.register(selector, xxx, object); //attachment被存放在返回的SelectionKey中 channel.keyFor(selector); //返回该channel在Selector上的注册关系所对应的SelectionKey。若无注册关系,返回null。
2、Slector内部实现原理:对所有注册的Channel进行轮询访问,一旦轮询到一个Channel 1有注册的事件发生,例如有数据来了,它就通过传回SelectionKey的方式来通知开发人员对Channel 1进行数据的读或写操作。
- Selector可以同时监控多个SelectableChannel的IO状况,是异步IO的核心。
-
Selector.open(); //静态方法,创建一个selector实例 selector.select(); /*selector通过调用select(),将注册的channel中有事件发生的取出来进行处理。监控所有注册的channel, 当其中有注册的IO操作可以进行时,该函数返回,并将对应的SelectionKey加入selected-key set。*/ selector.keys(); //所有注册在这个Selector上的channel selector.selectedKeys(); //所有通过select()方法监测到可以进行IO操作的channel
3、当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些你感兴趣的属性:
interest集合
ready集合
Channel
Selector
附加的对象(可选)
2.7 NIO采用Reactor(反应器)设计模式。
这个设计模式与Observer(观察者)设计模式类似。
- Observer:只能处理一个事件源;
- Reactor:可用来处理多个事件源。
2.8 总结
轮询的方式在处理多线程时不需要上下文切换
多线程的实现方式需要在线程之间进行上下文切换,有时也需要压栈弹栈操作。
因此NIO效率高
3. IO和NIO区别
3.1 IO是面向流的,NIO是面向缓冲区的
IO:每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
NIO:缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。增加了处理过程中的灵活性。
3.2 阻塞与非阻塞
IO:它的各种流是阻塞的。即,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO:非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
3.3 选择器(Selectors)
NIO的选择器允许一个单独的线程来监视多个输入通道。可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
参考:https://blog.csdn.net/duoduo18up/article/details/81875997#t6