Java IO
IO概述
根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型,分别是阻塞I/O模型、非阻塞I/O模型、I/O复用模型、信号驱动I/O模型、异步I/O模型。
- 阻塞I/O模型(blocking I/O):是最常用的I/O模型,缺省情形下,所有文件操作都是阻塞的。我们以套接字接口为例来理解此模型,即在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞I/O模型。
- 非阻塞I/O模型(nonblocking I/O):recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。
- 异步I/O模型(asynchronous I/O):告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作,异步I/O模型由内核通知我们I/O操作何时已经完成。
- I/O复用模型(I/O multiplexing):Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback。 *
- 信号驱动I/O模型(signal-driven I/O):首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。
java实现了BIO,NIO和AIO
IO特性:
1、先进先出,最先写入输出流的数据最先被输入流读取到。
2、顺序存取,可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile可以从文件的任意位置进行存取(输入输出)操作)
3、只读或只写,每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。
Java IO体系
FileInputStream/FileOutputStream 需要逐个字节处理原始二进制流的时候使用,效率低下
FileReader/FileWriter 需要组个字符处理的时候使用
StringReader/StringWriter 需要处理字符串的时候,可以将字符串保存为字符数组
PrintStream/PrintWriter 用来包装FileOutputStream 对象,方便直接将String字符串写入文件
Scanner 用来包装System.in流,很方便地将输入的String字符串转换成需要的数据类型
InputStreamReader/OutputStreamReader , 字节和字符的转换桥梁,在网络通信或者处理键盘输入的时候用
BufferedReader/BufferedWriter , BufferedInputStream/BufferedOutputStream , 缓冲流用来包装字节流后者字符流,提升IO性能,BufferedReader还可以方便地读取一行,简化编程。
Java NIO
Java NIO 由以下几个核心部分组成:
- Channel 通道
- Buffer 缓冲区
- Selector 选择器
在NIO中,我们是面向缓冲区(Buffer)编程的。其中Channel对应IO的Stream,Selector是因为NIO可以使用异步的非堵塞模式才加入的东西。
Channel可以理解为IO中的Stream,与Stream不同的是Channel是双向的,一个流只可能是InputStream或是OutputStream。Channel打开则可以进行读取,写入或读写。由于是Channel是双向的,因此,他能更好地反映出底层操作系统的真实情况;在Linux系统中,底层操作系统的通道就是双向的。
Buffer本身就是一块内存,底层实现上,实际就是一个数组,数组的读写都是通过Buffer来实现的,Buffer既可以用来读和写,与IO中的Stream不一样。
Channel和Buffer有好几种类型。下面是JAVA NIO中的一些主要Channel的实现:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
以下是Java NIO里关键的Buffer实现(7种原生数据类型都有各自对应的Buffer类型):
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
Buffer是特定基本类型元素的线性有限序列。除内容外,Buffer区的基本属性还包括capacity(容量)、limit(限制)和position(位置):
capacity是它所包含的元素的数量。缓冲区的容量不能为负并且不能更改。
limit是第一个不应该读取或写入的元素的索引。缓冲区的限制不能为负,并且不能大于其容量。
position是下一个要读取或写入的元素的索引。缓冲区的位置不能为负,并且不能大于其限制。
flip方法切换读写模式的时候会更改limit和position的位置
Java NIO引入了用于通道的缓冲区的ByteBuffer。 ByteBuffer有三个主要的实现:
- HeapByteBuffer
在调用ByteBuffer.allocate()时使用。 它被称为堆,因为它保存在JVM的堆空间中,因此你可以获得所有优势,如GC支持和缓存优化。 但是,它不是页面对齐的,这意味着如果你需要通过JNI与本地代码交谈,JVM将不得不复制到对齐的缓冲区空间。 - DirectByteBuffer
在调用ByteBuffer.allocateDirect()时使用。 JVM将使用malloc()在堆空间之外分配内存空间。 因为它不是由JVM管理的,所以你的内存空间是页面对齐的,不受GC影响,这使得它成为处理本地代码的完美选择。 然而,你要C程序员一样,自己管理这个内存,必须自己分配和释放内存来防止内存泄漏。 - MappedByteBuffer
在调用FileChannel.map()时使用。 与DirectByteBuffer类似,这也是JVM堆外部的情况。 它基本上作为OS mmap()系统调用的包装函数,以便代码直接操作映射的物理内存数据。