同步与异步
- 同步:调用者需要一直主动等待被调用者的结果。
- 异步:调用者调用被调用者后,调用者不会立刻得到结果,在调用者发起调用后,被调用者通过状态、通知或通过回调函数,让调用者知道结果。
因此,同步和异步的本质区别就在于调用者与被调用者之间结果消息通知机制的不同
, 一个是主动等待结果,一个是被动知道结果。
阻塞与非阻塞
- 阻塞:
- 需要内核IO操作
彻底完成
后才返回到用户空间执行用户程序的操作指令。 - 调用结果返回之前,该线程会被一直挂起,一直等待结果,不能继续,函数只有在得到结果之后才会返回。
- 需要内核IO操作
- 非阻塞:
- 用户空间的程序
不需要
等待内核IO操作彻底完成,可以立即返回用户空间去执行后续的指令。 - 在不能立即得到执行结果之前,该函数不会阻碍当前线程执行,而是会立即返回。
- 用户空间的程序
同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
用烧水举例。
- 同步/异步关注的是水烧开之后需不需要我来处理。
- 阻塞/非阻塞关注的是在水烧开的这段时间是不是干了其他事。
同步阻塞:
点火后,水开之前什么也不做(阻塞),水开之后手动关火(同步)。
同步非阻塞:
点火后,去看电视(非阻塞),时不时看水开了没有,水开后关火(同步)。
异步阻塞:
点火后,水开之前什么也不做(阻塞),水开后自动断电(异步)。
异步非阻塞:
点火后,该干嘛干嘛 (非阻塞),水开后自动断电(异步)。
什么是IO?
I/O(英语:Input/Output),即输入/输出,通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。
计算机视角理解IO
计算机硬件组成分为五大部分:控制器、运算器、存储器、输入和输出
。
其中输入是指将数据输入到计算机的设备,输出是指从计算机获取数据的设备。
对于计算机而言,任何涉及到计算机核心(CPU和内存)与其他设备间的数据转移的过程就是IO。
IO对于计算机而言有两层意思:
- IO设备,比如常见的鼠标、键盘;
- 对IO设备的数据读写;
程序视角理解IO
通过向内核发起系统调用完成对I/O的间接访问。
应用程序发起一次IO访问时分为两个阶段的:
- 数据准备阶段:内核等待IO设备准备好数据;
- 数据拷贝阶段:将数据从内核缓冲区拷贝到用户空间缓冲区;
操作系统的内存简介
操作系统的应用与内核
现代计算机是由硬件
和操作系统
组成,我们的应用程序要操作硬件(比如往磁盘上写数据),就需要先与内核交互,然后再由内核与硬件交互。
操作系统可以划分为:内核与应用两部分;
五种IO模型
同步IO操作:阻塞IO、非阻塞IO、IO复用、信号驱动式IO
异步IO操作:异步IO
阻塞IO(Blocking IO)
阻塞IO就是当应用A发起读取数据申请时,在内核数据还没准备好之前,应用A会一直处于等待数据状态,知道内核把数据准备好了交给应用A才结束。
我们之前所学过的所有的套接字,默认都是阻塞方式。

典型应用
阻塞socket
;
Java BIO
。
特点
- 进程阻塞挂起不消耗CPU资源,及时响应每个操作。
- 实现难度低,开发应用较容易。
- 使用并发量小的网络应用开发。
- 不适用并发量大的应用:因为一个请求IO会阻塞进程,所以得为每个请求分配一个处理进程(线程)以及时响应,系统开销大。
非阻塞IO(Nonblocking IO)
与阻塞IO不同的是,当内核数据还没准备好之前,进程不会被阻塞,内核会返回一个error给进程,采用轮询的方式检查内核数据是否准备好。

典型应用
socket
(设置为NONBLOCK)
特点
- 进程轮询(重复)调用,消耗CPU的资源。
- 实现难度低、开发应用相对阻塞IO模式较难。
- 适用并发量较小、且不需要及时响应的网络应用开发。
IO复用(IO Multiplexing)
基于非阻塞IO模型,我们知道,它需要进程不断轮询发起recvfrom
系统调用,十分消耗CPU。
而IO复用模型则不需要所有进程轮询,而是由复用器(select)
来询问内核。

多个进程的IO注册到一个复用器(select)
上,然后用一个进程监听该select,select会监听所有注册进来的IO。
如果内核数据报没有准备好,select调用进程会被阻塞,而当任一IO在内核缓冲区中有数据,select调用就会返回可读条件;
而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备的数据。
典型应用
select、poll、epoll
三种方案,nginx都可以选择使用这三个方案;
Java NIO
。
特点
- 专一进程解决多个进程IO的阻塞问题,性能好;
- 适用于高并发服务应用开发:一个进程(线程)响应多个请求;
- 模型复杂,实现、开发应用难度大。
信号驱动IO(Signal Driven IO)
当进程发起一个IO操作,会向内核注册一个信号处理函数
,然后进程返回不阻塞;
当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数
中调用IO读取数据。

特点
- 采用回调机制,等待数据阶段无阻塞;
- 适用于高并发应用程序;
- 模型较为复杂,实现难度大。
异步IO(Asynchromous IO)
信号驱动IO模型,经过三轮优化,终于不用在数据等待阶段阻塞了,但在数据复制节点依然是阻塞的。

当进程发送一个IO操作,进程会立刻返回(不阻塞),但是也不能返回结果;
内核会把整个IO数据报准备好后,再通知进程,进程再处理数据报。
特点
- 整个过程都不阻塞,一步到位;
- 非常适用高并发应用;
- 需要操作系统的底层支持,Linux2.5版本内核首现,2.6版本产品的内核标准特性;
- 模型复杂,实现、开发难度较大。
总结

什么是JAVA NIO?
N就是 Non-blocking,同步非阻塞IO,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

NIO的核心组件
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)

关系图说明:
- 每个
Channel
对应一个Buffer
。 - 一个
Selector
对应一个线程,一个线程对应多个Channel
。 - 上图反应了有三个
Channel
注册到该Selector
。 - 程序切换到那个
Channel
是由事件决定的(Event)。 Selector
会根据不同的事件,在各个通道上切换。Buffer
就是一个内存块,底层是有一个数组。- 数据的读取和写入是通过
Buffer
,但是需要flip()
切换读写模式,而BIO
是单向的,要么输入流要么输出流。
Channel(通道)
Channel
是一个对象,作用是用于源节点和目标节点的连接,在java NIO中负责缓冲区数据的传递。Channel本身不存储数据,因此需要配合缓冲区进行传输。
传统的IO是基于流操作的,Channel和它类似,但又有不同。
区别 | 传统IO | 通过Channel |
---|---|---|
支持异步 | 不支持 | 支持 |
是否可双向传输数据 | 不能,只能单向 | 可以,既可以从通道读取数据,也可以向通道写入数据 |
是否结合 Buffer 使用 | 否 | 必须结合 Buffer 使用 |
性能 | 较低 | 较高 |
Channel 必须要配合 Buffer 一起使用,我们永远不可能将数据直接写入到 Channel 中,同样也不可能直接从 Channel 中读取数据。都是通过从 Channel 读取数据到 Buffer 中或者从 Buffer 写入数据到 Channel 中。

简单点说,Channel 是数据的源头或者数据的目的地,用于向 buffer 提供数据或者读取 buffer 数据,并且对 I/O 提供异步支持。
主要实现类
主要的实现类有:FileChannel, SocketChannel, ServerSocketChannel, DatagramChannel
。
FileChannel
本地文件IO通道,用于读取、写入、映射和操作文件的通道。
SocketChannel
网络套接字IO通道,TCP协议,针对面向流的连接套接字的可选择通道(一般用在客户端)。
ServerSocketChannel
网络通信IO操作,TCP协议,针对面向流的监听套接字的可选择通道(一般用于服务端)。
DatagramChannel
数据报通道,能通过 UDP 读写网络中的数据。
Buffer(缓冲区)
Buffer
是一个数据对象,我们可以把它理解为固定数量的数据的容器,它包含一些要写入或者读出的数据。
在 Java NIO 中,任何时候访问 NIO 中的数据,都需要通过缓冲区(Buffer)进行操作。读取数据时,直接从缓冲区中读取,写入数据时,写入至缓冲区。NIO 最常用的缓冲区则是 ByteBuffer
。

每一个 Java 基本类型都对应着一种 Buffer,他们都包含这相同的操作,只不过是所处理的数据类型不同而已。
Selector(多路复用器)
多路复用器 Selector
,它是 Java NIO 编程的基础,它提供了选择已经就绪的任务的能力。
从底层来看,Selector
提供了询问通道是否已经准备好执行每个 I/O 操作的能力。
简单来讲,Selector
会不断地轮询注册在其上的 Channel,如果某个 Channel 上面发生了读或者写事件,这个 Channel 就处于就绪状态,会被 Selector
轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。
Selector
允许一个线程处理多个 Channel ,也就是说只要一个线程复杂 Selector
的轮询,就可以处理成千上万个 Channel ,相比于多线程来处理势必会减少线程的上下文切换问题。