一次带你搞懂Java中的BIO|NIO|AIO,你也可以轻松玩转!
# BIO
Java中BIO也成为同步阻塞IO。
同步阻塞IO模式下,服务器实现模式为一个连接对应一个线程,即:有连接请求从客户端发起时,服务器端就需要创建一个线程进行处理,如果有大量连接时,服务器就需要创建大量线程进行处理。当然可以通过线程池机制改善。
阻塞IO适用场景为:连接数较小且固定的架构模式,这种方式对服务器资源要求比较高,并发局限于应用中,不建议在生产环境使用。
# NIO(非阻塞IO)
Java中非阻塞式IO,也叫同步非阻塞IO,其通过通道和缓冲区来读写数据。通过选择器注册和获取已准备好的感兴趣的通道事件。读数据时,有多少数据就读多少数据,读完立即返回,如SocketChannel和ServerSocketChannel、Selector等。
NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题。NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。
也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
NIO的最重要的地方是,当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面。所以,所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题。
Java NIO 由以下几个核心部分组成:
- Channels
- Buffers
- Selectors
虽然Java NIO 中除此之外还有很多类和组件,但是,Channel,Buffer和Selector构成了核心的API。其它组件,如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类。
读者推荐:[并发编程学习笔记](https://mp.weixin.qq.com/s/B6YzNvAopKkFxKf-1Q_NcA)!
# Channel vs Buffer
Channel和Buffer:基本上,所有的IO在NIO中都从一个Channel开始。Channel有点像流,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中。如:Channel和Buffer有好几种类型。下面是JAVA NIO中的一些主要Channel的实现:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel。这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。
![](https://upload-images.jianshu.io/upload_images/24630328-884613e4f0d8b8bc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
Java NIO里关键的Buffer实现:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer(内存映射文件 )。这些Buffer覆盖了能通过IO发送的基本数据类型:byte,short,int,long,float,double和char。
Channel和Buffer使用案例:改代码以随机读写方式打开nio-data.txt文件,一个字节一个字节的把文件内容读入新分配的buf缓存中。
![](https://upload-images.jianshu.io/upload_images/24630328-6798f47e7f66cca5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
# Selector
Selector允许单线程处理多个 Channel。如果应用打开了多个**连接**(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。单线程中使用一个Selector处理3个Channel的图示:
![](https://upload-images.jianshu.io/upload_images/24630328-6ca9da4611d26c77.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
要使用Selector,得向Selector注册Channel,然后调用它的select()方法。**这个方法会一直阻塞**到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
Selector使用案例:该案例使用Selector已订阅事件的通道进行选择,当订阅事件发生时即可另行开辟线程处理该通道事件。
![](https://upload-images.jianshu.io/upload_images/24630328-1b5d10b447bbf5dc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
# AIO(异步IO)
Java中的异步IO,通过Future轮询和Callback回调两种方式来使用。
与NIO不同,当进行读写操作时,异步IO情况下,我们只需要直接调用API的read或write方法即可。这两个操作都是异步的。
- 对于读操作而言,当有流可读时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;
- 对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:AsynchronousSocketChannel、
AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。
# Future轮询模式
典型的Java通道类有AsynchronousFileChannel。来看一个例子:
![](https://upload-images.jianshu.io/upload_images/24630328-26acaa3cc33a3868.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
该案例通过异步IO通道AsynchronousFileChannel将文件foobar.txt的内容读入buffer中,在通过Future获取结果。Java将读取文件的操作交给操作系统底层去做,自己只需要知道文件是否读取完毕就好了。其实底层JVM为执行这个任务创建了线程池和通道组。具体可以参考AsynchronousFileChannel的官方说明:
> An AsynchronousFileChannel is associated with a thread pool to which tasks are submitted to handle I/O events and dispatch to completion handlers that consume the results of I/O operations on the channel. The completion handler for an I/O operation initiated on a channel is guaranteed to be invoked by one of the threads in the thread pool (This ensures that the completion handler is run by a thread with the expected identity). Where an I/O operation completes immediately, and the initiating thread is itself a thread in the thread pool, then the completion handler may be invoked directly by the initiating thread. When an AsynchronousFileChannel is created without specifying a thread pool then the channel is associated with a system-dependent default thread pool that may be shared with other channels. The default thread pool is configured by the system properties defined by the AsynchronousChannelGroup class.
# Callback回调模式
Future其实本质上还是轮循的方式,回调式才是真正的AIO。其基本思想是主线程注册一个**CompletionHanlder**执行IO操作。**CompletionHanlde**将带着IO操作的结果返回到主线程中,这个结果会触发它自己的completed或者failed方法(需要重写这两个方法):
1. void completed(V result, A attachment) - executes if a task completes with a result of type V.
2. void failed(Throwable e, A attachment) - executes if the task fails to complete due to Throwable e.
callback回调案例:
![](https://upload-images.jianshu.io/upload_images/24630328-8c706b39b1e3e19b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
上面的例子是基于文件的AsynchronousFileChannel,但是基于网络套接字的
AsynchronousServerSocketChannel和AsynchronousSocketChannel也是一样的模式。