本文旨在理解Java IO的几种类型,不会过多深入讲解,不过笔者已经计划用一个系列来讲解IO,敬请期待。。。
1、Java IO 简介
在计算机系统中I/O就是输入(Input)和输出(Output)的意思,针对不同的操作对象,可以划分为磁盘I/O模型,网络I/O模型,内存映射I/O, Direct I/O、数据库I/O等,只要具有输入输出类型的交互系统都可以认为是I/O系统,也可以说I/O是整个操作系统数据交换与人机交互的通道,这个概念与选用的开发语言没有关系,是一个通用的概念。
Java IO 是一套Java用来读写数据(输入和输出)的API。大部分程序都要处理一些输入,并由输入产生一些输出。Java为此提供了java.io包。
Java IO可以分为 BIO,NIO,AIO 三种IO模型,这里先给出简单的描述,下文会对这三种模型分别介绍,以达到简单理解的目的。
BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
NIO (New I/O): 同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
AIO ( Asynchronous I/O): 异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
2、BIO(同步阻塞I/O模型)
2.1、什么是BIO
BIO(Blocking I/O) 是同步阻塞IO,JDK1.4之前只有这一个IO模型,BIO操作的对象是流,一个线程只能处理一个流的IO请求,如果想要同时处理多个流就需要使用多线程。
流包括字符流和字节流,流从概念上来说是一个连续的数据流。当程序需要读数据的时候就需要使用输入流读取数据,当需要往外写数据的时候就需要输出流。
在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,ServerSocket 负责绑定IP地址,启动监听端口,Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。
2.2、BIO 阻塞IO模型
在Linux中,当应用进程调用recvfrom方法调用数据的时候,如果内核没有把数据准备好不会立刻返回,而是会经历等待数据准备就绪,数据从内核复制到用户空间之后再返回,这期间应用进程一直阻塞直到返回,所以被称为阻塞IO模型
2.3、BIO网络编程
当使用BIO模型进行Socket编程的时候,服务端通常使用while循环中调用accept方法,在没有客户端请求时,accept方法会一直阻塞,直到接收到请求并返回处理的相应,这个过程都是线性的,只有处理完当前的请求之后才会接受处理后面的请求,这样通常会导致通信线程被长时间阻塞。
在这种模式中我们通常用一个线程去接受请求,然后用一个线程池去处理请求,用这种方式并发管理多个Socket客户端连接,如下图:
使用BIO模型进行网络编程的问题在于缺乏弹性伸缩能力,客户端并发访问数量和服务器线程数量是1:1的关系,而且平时由于阻塞会有大量的线程处于等待状态,等待输入或者输出数据就绪,造成资源浪费,在面对大量并发的情况下,如果不使用线程池直接new线程的话,就会大致线程膨胀,系统性能下降,有可能导致堆栈的内存溢出,而且频繁的创建销毁线程,更浪费资源
使用线程池可能是更优一点的方案,但是无法解决阻塞IO的阻塞问题,而且还需要考虑如果线程池的数量设置较小就会拒绝大量的Socket客户端的连接,如果线程池数量设置较大的时候,会导致大量的上下文切换,而且程序要为每个线程的调用栈都分配内存,其默认值大小区间为 64 KB 到 1 MB,浪费虚拟机内存
BIO模型适用于链接数目固定而且比较少的架构,但是使用这种模型写的代码更直观简单易于理解
3、AIO(异步非阻塞I/O模型)
3.1、什么是AIO(Asynchronous I/O)
Java AIO就是Java作为对异步IO提供支持的NIO.2 ,Java NIO2 (JSR 203)定义了更多的 New I/O APIs, 提案2003提出,直到2011年才发布, 最终在JDK 7中才实现。JSR 203除了提供更多的文件系统操作API(包括可插拔的自定义的文件系统), 还提供了对socket和文件的异步 I/O操作。 同时实现了JSR-51提案中的socket channel全部功能,包括对绑定,option配置的支持以及多播multicast的实现。
从编程模式上来看AIO相对于NIO的区别在于,NIO需要使用者线程不停的轮询IO对象,来确定是否有数据准备好可以读了,而AIO则是在数据准备好之后,才会通知数据使用者,这样使用者就不需要不停地轮询了。当然AIO的异步特性并不是Java实现的伪异步,而是使用了系统底层API的支持,在Unix系统下,采用了epoll IO模型,而windows便是使用了IOCP模型。
3.2、AIO 异步IO模型
在Linux系统中,应用进程发起read操作,立刻可以去做其他的事,内核会将数据准备好并且复制到用空间后告诉应用进程,数据已经复制完成read操作
3.3、AIO 网络编程
AIO 不需要通过多路复用器对注册的通道进行轮询操作就可以实现异步读写,从而简化了NIO的编程模型
AIO 通过异步通道实现异步操作,异步通道提供了两种方式获取操作结果:
- 通过Future类来获取异步操作的结果,不过要注意的是future.get()是阻塞方法,会阻塞线程
- 通过回调的方式进行异步,通过传入一个CompletionHandler的实现类进行回调,CompletionHandler定义了两个方法,completed和failed两方法分别对应成功和失败
AIO 中的Channel 支持以上两种方式,AIO提供了对应的异步套接字通道实现网络编程,服务端:AsynchronousServerSocketChannel和客户端AsynchronousSocketChannel
4、NIO(同步非阻塞I/O模型)
4.1、什么是NIO
Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。
4.2、NIO 的核心概念
4.2.1、Java NIO: Channels (通道)
Channel(通道)数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中,Channel只负责运输数据,而操作数据是Buffer
通道与流类似,不同之处在于:
- 通道是双向的,可以同时进行读,写操作,而流是单向流动的,只能写入或者读取
- 流的读写是阻塞的,通道可以异步读写
Channel 有四种实现:
- FileChannel: 是从文件中读取数据。
- DatagramChannel: 从UDP网络中读取或者写入数据。
- SocketChannel: 从TCP网络中读取或者写入数据。
- ServerSocketChannel: 允许你监听来自TCP的连接,就像服务器一样。每一个连接都会有一个SocketChannel产生。
4.2.2、Java NIO: Buffers(缓冲区)
Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
Buffer是一个对象。它包含一些要写入或者读出的数据。在面向流的I/O中,可以将数据写入或者将数据直接读到Stream对象中,所有的数据都是用缓冲区处理。
缓冲区实质是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。
最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean)都对应一种缓冲区,具体如下:
- ByteBuffer:字节缓冲区
- CharBuffer:字符缓冲区
- ShortBuffer:短整型缓冲区
- IntBuffer:整型缓冲区
- LongBuffer:长整型缓冲区
- FloatBuffer:浮点型缓冲区
- DoubleBuffer:双精度浮点型缓冲区
4.2.3、Java NIO: Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。节省线程切换上下文的资源消耗。Selector 只能管理非阻塞的通道,FileChannel是阻塞的,无法管理。
Selector与Channel,Buffer之间的关系
关键对象
- Selector:选择器对象,通道注册、通道监听对象和Selector相关。
- SelectorKey:通道监听关键字,通过它来监听通道状态。
监听注册
监听注册在Selector
socketChannel.register(selector, SelectionKey.OP_READ);
监听事件:
- OP_ACCEPT: 接收就绪,serviceSocketChannel使用的
- OP_READ: 读取就绪,socketChannel使用
- OP_WRITE: 写入就绪,socketChannel使用
- OP_CONNECT: 连接就绪,socketChannel使用
5、IO与NIO区别
6、同步与异步的区别
同步: 发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生。
异步: 发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发。
7、阻塞与非阻塞的区别
阻塞: 传统的IO流都是阻塞式的。当一个线程调用read()或者write()方法时,该线程将被阻塞,直到有一些数据读取或者被写入,在此期间,该线程不能执行其他任何操作。在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量的客户端时,性能急剧下降。
非阻塞: Java NIO是非阻塞式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程会去执行其他任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此NIO可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
8、Java对BIO、NIO、AIO的支持
Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
9、BIO、NIO、AIO适用场景分析
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
参考资料:
- http://ifeve.com/java-nio-all/
- https://segmentfault.com/a/1190000014932357
- https://github.com/Snailclimb/JavaGuide
- http://tutorials.jenkov.com/java-io/overview.
- https://www.baeldung.com/java-io
- http://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-vs-io.html
- http://cmsblogs.com/?p=4812
- https://youzhixueyuan.com/java-nio-introduce.html
- https://www.journaldev.com/942/java-io-tutorial
- https://juejin.im/post/5d1acd6b6fb9a07eaa2294d0