1. 阻塞与非阻塞
"阻塞"与“非阻塞”概念经常和“同步”、“异步”混淆。在Java程序中,很多线程通常处于阻塞(blocking)状态,而同步(并不是指多线程同步的Synchronized)并不是这样,同步通常是指步骤需要一步步来完成,就想常规的代码一条条地执行一样,但异步可以在没有执行完当前这行代码之前,就执行下一行代码,就像很多JS代码、UI控件、后台启动线程等。
相对于阻塞来讲,同步的程序线程应当是处于Running状态的。线程处于Blocking状态就差不多可以看成是睡眠的,也就是说它什么也没法做,只有一直等待一个信号的产生才能唤醒。处于Running状态下的线程是活跃的,在这种情况下可以去做很多其他的事情,可以一直去检测一些事情是否做好。
2. BIO (Blocking I/O)
Blocking I/O(阻塞I/O),这是最古老的Java通信机制,在这种模型中,应用程序首先会通过System Call发送请求给内核(Kernel),然后由内核去进行网络通信,应用程序如果发起的系统调用是读操作,在内核准备好数据以前,这个线程将会被挂起,一直等待下去,直到有返回的数据在内核中准备好位置,或者在设置SoTimeout后超时被唤醒。
假如抛开超时机制在网络等待机制中的作用,发起read()操作等待远程返回数据的过程由以下两个阶段组成。
阶段1:等待I/O返回数据,这取决于I/O请求的目标返回数据的速度。例如在网络I/O请求中,在远程返回数据前也需要经过一些处理才返回数据,远程输出数据后将取决于网络本身的速度及数据本身大小。
阶段2:返回数据首先被填充到内核(Kernel)的缓冲区里,然后再从内核区将数据向进程内部拷贝(copy data from kernel to user)。这个过程完成后,应用程序才会继续向下执行。
根据上面的过程,在BIO过程中,程序与内核交互的过程如下图所示:
2. NIO (New I/O)
Java从1.4版本开始支持NIO(New IO),可以实现非阻塞I/O的。非阻塞I/O和阻塞I/O有个很明显的区别是:当发生第一次System Call请求后,线程并没有被阻塞,但它没有做别的事情,而是在不断发起System Call请求。
这样操作似乎在空耗CPU,给人感觉还不如一直挂起等待返回,那样至少会让出CPU资源。其实,每次System Call只是看看数据准备好没有,通常它的时间是很短暂的,这样的动作完全只用一个线程来完成对很多事件的监听。换句话说,其它的线程可以去干别的事,只需要某个线程定期来做下检测即可,在设计上注意这个检测频率,就可以达到效率高且节约资源的目的。
3. AIO (Asynchronous I/O)
JDK 1.7引入了NIO 2.0,也就是AIO,又叫异步IO。下面用“物流送货”来比如Java中的IO模型:
① BIO:你需要到物流的中转站去等货且不能离开中转站,如果货没到,其他的事情就别想做。
② NIO:每天去检测一下货物是否到了,这个动作十分简单,可以让每个小区派1个人去看看有没有这个小区的货物,如果有就带回来,或者这个人通知你去物流中转站拿货。
③ AIO:货到的时候送货上门,换句话说,去拿货的路途虽然不长,但是由别人帮你承担了。
使用AIO读取文件并不代表读取更快,我们不能用AIO读取一个大文件与用BIO来对比速度,这样没有任何意义。因为AIO的目的在于I/O过程中程序去做别的事情,也就是在并发时,更少的资源可以做更多的事情,并不是看在I/O过程中谁更快,它也做不到这点,因为能做到I/O更快只有提高磁盘或者网络本身的速度,而这些I/O模型只是调度I/O的机制而已。
AIO相对NIO,在某种意义上讲更加提高了资源利用率,但这仅仅是相对进程本身而言,对于整个服务器来说,还得看实际情况,因为进程不想做的事情交给内核去做了。AIO应当更加适合运用在I/O密集型系统中,BIO也并非没有价值,它使得交互简化,而且在很多情况下本身没有必要使用异步和非阻塞。