Java IO对大多数Java程序员来说是熟悉又陌生,熟悉的是感觉到处都有它的身影,小到简单的读取文件,大到各种服务器的应用,陌生的是Java IO背后到底是一个怎样的机制,今天就让我们去了解一下这位老朋友吧。本文不讲解Java IO如何具体使用,有这方面需求的同学可以自己查下。

IO 模型

要说IO,就不得不说IO模型,IO模型大家都有所了解,同步异步,阻塞非阻塞什么的,总的来说IO模型可分为以下五种:

  • 阻塞IO
  • 非阻塞IO
  • 多路复用IO
  • 信号驱动IO
  • 异步IO

那么这几种IO都有什么区别呢?下面我们一一来看,每种模型我都会举一个适当的例子助于理解:

1.阻塞 IO

阻塞IO相信大家都最熟悉了,线程发起一个IO请求,直到有结果返回,否则则一直阻塞等待,比如我们平常常见的阻塞数据库操作,网络IO等。

小明阻塞IO吃饭:

五年前一天周末,小明和朋友一起去商场的外婆家吃饭,到店后发现排队的人超多,所以他就领了一个号码,然后他和朋友就坐在旁边等候,一直等着服务员叫他们的号,也不能做其他事,过了一个多小时终于轮到他们了,然后他们进店点菜,又得等待上菜,最后他们吃饭总共花了两个小时;

关键部分:

  • 等待座位吃饭:一直阻塞,直到有座位
  • 等待上菜:一直阻塞,直到有菜(假设菜上齐了再吃)

没什么说的,反正就是一直等,反应到程序中就是一直阻塞,而一个IO请求需要一个线程,可想而知当有大量的IO请求,线程的创建和销毁,线程间的切换,线程所占用的资源等等要耗费多少时间和资源,系统的性能会有多差。

2.非阻塞 IO

非阻塞IO和阻塞IO的最大区别就在于线程发起一个IO请求,不会一直堵塞直到有数据,而是不断的检查是否已有数据,若有数据则读取数据。

小明非阻塞IO吃饭:

有了第一次的教训,小明学乖了,他在拿到后不再傻傻的等着,而是去外婆家旁边逛了逛,每过3分钟他就会回来,然后跑到前台去询问服务员轮到他了吗?不幸的是,排队的人超多,直到过了半个多小时后才轮到他进店吃饭,期间他大概问了十几次,他们进店点菜,又得等待上菜,最后他们吃饭总共花了两个小时,基本也没做啥其他事;

关键部分:

  • 领号后询问是否轮到他:非阻塞,非询问期间可以做点别的事,但也不做了啥大事
  • 等待上菜:一直阻塞,直到有菜(假设菜上齐了再吃)

总的来说非阻塞IO的非阻塞主要体现在不需要一直等待到有数据,当然读数据那部分操作还是阻塞的,另外这种非阻塞模式需要用户线程自己不断询问检查,其实效率也不是太高,实际编程中运用的也不多。

3.多路复用 IO

既然上面我们说到非阻塞IO的缺点,那么有没有什么方式改进呢?答案是当然有,那就是多路复用IO,我理解的它的特点就是复用,首先它也是一种非阻塞IO的模型,只不过上面说到轮询的方式用了不同的方式处理了,当一个线程发起IO请求,系统会将它注册到一个单独管理IO请求的一个线程,之后该IO的相关操作的通知状态都有这个管理IO请求的线程处理,Java 1.4发布的NIO就是这种模式,我们可以大致来看一下它的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 打开服务器套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 服务器配置为非阻塞
ssc.configureBlocking(false);
// 进行服务的绑定
ssc.bind(new InetSocketAddress("localhost", 8008));
// 这里的selector就相当于单独管理IO请求的线程
Selector selector = Selector.open();
// 注册到selector,等待连接
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); //为IO请求去轮询状态
Set<SelectionKey> keys = selector.selectedKeys(); //多个IO请求的状态
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) { //依次处理IO请求
SelectionKey key = keyIterator.next();
doThing(key)
...
}
}

可以看出Java NIO的模式就是多路复用IO模型的应用。

小明多路复用IO吃饭:

随着生意越来越好,外婆家发现好多顾客都堵在门口等待吃饭,等待区都站不下来人了,,思来想去,外婆家准备请一个人专门来维护顾客的排队请求,这样顾客取号后,就不用堵在门口了,我们叫他小A,小明这次取号后,将自己的相关信息告诉小A,并从小A那里获得了一个GPS(用于小A能快速找到小明,假设有了GPS后,小A能秒速找到小明),然后小明就跟朋友们开心的去逛商场,看看MM,买买衣服,而小A则不断的观察店里的情况,当有空座位出现的时候,他便会按照相关信息找到具体的顾客,将其带回进行用餐,但他们进店点菜,还得等待上菜,最后他们吃饭总共花了两个小时,但是他们不再需要排队等位,而是去做一些其他的事。

关键部分:

  • 领号后委托给小A,小A观察到有空位后带回小明:非阻塞,领号后可以安心去做自己的事,不用担心错过
  • 等待上菜:一直阻塞,直到有菜(假设菜上齐了再吃)

多路复用IO可以看成普通非阻塞IO的升级版,也是目前Java编程中用到比较多的IO模型,它的优势在于可以处理大量的IO请求,用一个线程管理所有的IO请求,无需像阻塞IO和非阻塞IO一样,每个IO需要一个线程处理,提升了系统的吞吐量。

4.信号驱动 IO

信号驱动IO相对于以上几种模型最大的特点就是它支持内核信号通知,线程在发起一个IO请求后,会注册一个信号函数,然后内核在确认数据可读了,便会给相应的线程发送通知,让其进行具体IO读写操作。

小明信号驱动IO吃饭:

又了一段时间,外婆家通过使用复用IO模式缓解了排队拥挤的情况,但是觉得还要请一个人专门维护队列,感觉不划算,那么有没有一种更好的方式呢?经过一天的苦思冥想,外婆家的经理又想出一个好办法,让每个顾客在领完号后,关注一下外婆家的公众号,然后顾客就可以去做别的事了,定时或者当排队信息发生改变时给顾客发送通知,告知他现在的排队序号或者轮到他吃饭了,顾客可以根据相应的信息做相应的行为,比如快轮到了就开始往店里走(实际程序中并不一定有这种状态,这里只是大概模拟),或者轮到自己了然后进店吃饭,他们仍然不用排队等位,而是去做一些其他的事。

关键部分:

  • 领号后关注公众号,注册关系:非阻塞,领号后可以安心去做自己的事,不用担心错过
  • 等待上菜:一直阻塞,直到有菜(假设菜上齐了再吃)

就实际来说,信号驱动IO用的并不多,因为信号驱动IO底层是使用SIGIO信号,所以它主要使用在UDP协议上,因为UDP产生SIGIO信号的时候只有两种可能:

  • 1.要么数据到达
  • 2.发生错误

但相对TCP来说,产生SIGIO信号的地方太多了,比如请求连接,确认,断开,错误等等,所以我们很难根据SIGIO信号判断到底发生了什么。

5.异步 IO

以上四种IO其实都还是同步IO,因为它们在读写数据时都是阻塞的,异步IO相较于它们最大的特点是它读写数据的时候也是非阻塞的,用户线程在发起一个IO请求的时候,除了给内核线程传递具体的IO请求外,还会给其传递数据缓冲区,回调函数通知等内容,然后用户线程就继续执行,等到内核线程发起相应通知的时候,说明数据已经准备就绪,用户线程直接使用即可,无需再阻塞从内核拷贝数据到用户线程。

小明异步IO吃饭:

有过了一段时间,小明又想吃外婆家了,但是这个周末他并不想出门,他突然在网上看到新闻说外婆家竟然可以叫外卖,小明高兴坏了,他马上打电话给外婆家,告诉它自己想要吃哪些菜(相当于IO请求所需要的数据),然后将自己的联系号码(相当于回调通知)和住址(相当于数据缓冲区)也告诉它,然后就挂掉电话,开心的做去打游戏了,过了半个小时后,手机响起,告知外卖已经到了,小明开门取外卖就可以直接开吃了。整个过程小明直到吃饭都没有等待阻塞。

关键部分:

  • 叫外卖并提供相应的信息:非阻塞,打完电话后做自己的事
  • 通知外卖到了:直接开门取外卖直接开吃,非阻塞

我们可以看出,异步IO才是真正的异步,因为它连数据拷贝这个过程都是非阻塞的,用户线程根本不用关心数据的读写等操作,只需等待内核线程通知后,直接处理数据即可,当然异步IO需要系统内核支持,比如Linux中的AIO和Windows中的IOCP,但是也可以通过多线程跟阻塞I/O模拟异步IO,比如可以在多路复用IO模型上进行相应的改变,另外也有现有的实现,比如异步I/O的库:libeio

最后用一张图总体概括一下Java IO(图片来自美团技术博客):

Java IO概图:

java-io

多路复用 IO 在 Linux 中的实现

因为后续会讲到Java NIO,所以我们需要了解操作系统是如何支持多路复用IO的,Linux中支持支持三种多路IO复用机制,分别是select、poll和epoll,本来这里我想自己写的,但查阅了相应的一些资料后,发现自己的水平还是不够,这里我不准备班门弄斧了,因为我找到了很多写的比较好的文章,这里就给大家列一下,仅供参考:

总结

这篇文章主要讲了最基础的IO模型,不过我认为最基础的往往是最重要的,只有理解了基础的原理,才能对基于它们实现的类库或者工具有更加深刻的认识,下一篇文章将会主要讲一下基于多路复用IO的Java NIO,敬请期待。