JAVA IO
Java IO 即 Java 输入输出。在开发应用软件时,很多时候都需要和各种输入输出相关的媒介打交道。与媒介进行 IO 操作的过程十分复杂,需要考虑众多因素,比如:进行 IO 操作媒介的类型(文件、控制台、网络)、通信方式(顺序、随机、二进制、按字符、按字、按行等等)。
Java 类库提供了相应的类来解决这些难题,这些类就位于 java.io 包中, 在整个 java.io 包中最重要的就是 5 个类和一个接口。5 个类指的是 File、OutputStream、InputStream、Writer、Reader;一个接口指的是 Serializable。
由于老的 Java IO 标准类提供 IO 操作(如 read(),write())都是同步阻塞的,因此,IO 通常也被称为阻塞 IO(即 BIO,Blocking I/O)。
JAVA-NIO
在 JDK1.4 之后,为了提高 Java IO 的效率,Java 又提供了一套 New IO(NIO),原因在于它相对于之前的 IO 类库是新增的。此外,旧的 IO 类库提供的 IO 方法是阻塞的,New IO 类库则让 Java 可支持非阻塞 IO,所以,更多的人喜欢称之为非阻塞 IO(Non-blocking IO)。
*注意:异步只有异步,同步才有阻塞和非阻塞的说法!
IO模型
- 阻塞式IO(blocking IO),即传统的IO模型
- 非阻塞式IO( non-blocking IO),默认创建的socket都是阻塞的
- IO多路复用(IO multiplexing) ,即经典的Reactor设计模式,有时也称为异步阻塞IO
- 异步IO(asynchronous IO),即经典的Proactor设计模式,也称为异步非阻塞IO
- 信号驱动式IO(signal driven IO)
IO操作,主要分为两部分
- 数据准备,将数据加载到内核缓存(数据加载到操作系统)
- 将内核缓存中的数据加载到用户缓存(从操作系统复制到应用中)
同步和异步的概念描述的是用户线程与内核的交互方式。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式。
同步阻塞IO
同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞
用户线程通过系统调用read发起IO读操作,由用户控件转到内核空间。
内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。
同步非阻塞IO
同步阻塞IO是在同步阻塞IO的基础上,将socket设置为NIO(NOBLOCK)。
这样做用户线程可以在发起IO请求后可以立即返回。
由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。
但未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取数据,继续执行。
IO多路复用
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的。
使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。
当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,
甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。
但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。
用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。
而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
然而,使用select函数的优点并不仅限于此。
虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。
如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。
IO多路复用模型使用了Reactor设计模式实现了这一机制。
首先,要从你常用的IO操作谈起,比如read和write,通常IO操作都是阻塞I/O的,也就是说当你调用read时,
如果没有数据收到,那么线程或者进程就会被挂起,直到收到数据。
这样,当服务器需要处理1000个连接的的时候,而且只有很少连接忙碌的,
那么会需要1000个线程或进程来处理1000个连接,而1000个线程大部分是被阻塞起来的。
由于CPU的核数或超线程数一般都不大,比如4,8,16,32,64,128,比如4个核要跑1000个线程,那么每个线程的时间槽非常短,而线程切换非常频繁。
这样是有问题的:
1,线程是有内存开销的,1个线程可能需要512K(或2M)存放栈,那么1000个线程就要512M(或2G)内存。
2,线程的切换,或者说上下文切换是有CPU开销的,当大量时间花在上下文切换的时候,分配给真正的操作的CPU就要少很多。
那么,我们就要引入非阻塞I/O的概念,非阻塞IO很简单,通过fcntl(POSIX)或ioctl(Unix)设为非阻塞模式,
这时,当你调用read时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,如EWOULDBLOCK。
这样是不会阻塞线程了,但是你还是要不断的轮询来读取或写入。
于是,我们需要引入IO多路复用的概念。
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,
如果有一个文件描述符就绪,则返回,否则阻塞直到超时。
得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,
这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。
异步IO
“真正”的异步IO需要操作系统更强的支持。
在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。
而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
异步IO模型使用了Proactor设计模式实现了这一机制。
相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。
况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式
(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。
Java7之后已经支持了异步IO,感兴趣的读者可以尝试使用。