IO与NIO的区别
java.io:以阻塞的方式处理输入输出。
java.nio :以非阻塞的方式处理IO操作。
.
java.io 中最为核心的概念是流(Stream),面向流的编程。Java中,一个流要么是输入流,要么是输出流,不可能同时及是输入流又是输出流。
java.nio中拥有3个核心概念:Selector、Channel与Buffer。在java.nio中,我们是面向块(block)或是缓冲区(buffer)编程的。Buffer本身就是一块内存,底层实现上,它实际上是个数组,数据的读、写都是通过Buffer来实现的。
.
io中数据直接从stream流读到我们的程序中。
nio中是从channel中读到buffer中,之后我们再读取buffer(数组)中的内容,读到到我们的程序中。我们还可以将数据写回到buffer中。buffer不仅可以读还可以回写。 进行读写切换之前必须调用filp()方法。
如图:一个线程对应了一个selector,而一个selector对应了三个channel,每个channel又分别对应了一个Buffer。一个线程可以不断的在三个channel上来回切换,具体某一时刻切换到哪?通过事件决定的,在NIO中Event(事件)是一个非常重要的概念。【类似于我们客户端通过Ajax编程的时候,通过事件来判断并执行一个一个事情。】
什么是Channel? 可以将nio中的Channel理解为io中的stream【通道】
除了数组之外,Buffer还提供了对于数据的结构化访问方式,并且可以追踪到系统的读写过程。
Java中的7种原生数据类型都有各自对应的Buffer类型,如IntBuffer、LongBuffer、ByteBuffer及CharBuffer等等。并没有BooleanBuffer
Channel指的是可以向其写入数据或是从中读取数据的对象,它类似于java.io中的Stream。
所有数据的读写都是通过Buffer来进行的,永远不会出现直接向Channel写入数据的情况,或是直接从Channel读取数据的情况。
与Stream不同的是,Channel是双向的,一个流只可能是InputStream或是OutputStream,
Channel打开后则可以进行读取、写入或是读写。
由于Channel是双向的,因此它能更好地反映出底层操作系统的真实情况;在Linux系统中,底层操作系统的通道就是双向的。
关于NIO Buffer中的3个重要状态属性的含义:position,limit与capacity。
0 <= mark <= position <= limit <= capacity
通过NIO读取文件设计到3个步骤
#相对操作
相对操作:从当前位置开始读取或写入一个或多个元素,然后按所转移的元素数量增加位置。如果请求的传输超过了限制,那么相对get操作抛出{@link BufferUnderflowException},相对put操作抛出{@link BufferOverflowException};在这两种情况下,都不会传输数据。
#绝对操作
绝对操作:采用显式元素索引,不影响位置。绝对get和put操作抛出一个
IndexOutOfBoundsException}如果索引参数超过限制。
当然,数据也可以通过通道的I/O操作(总是相对于当前位置)被传送到缓冲区或从缓冲区中传送出去。
mark与reset
mark和reset:当调用reset()方法时,缓冲区的mark是将其位置重置到的索引。mark并不总是被定义的,但当它被定义时,它永远不会为负,也不会比position大。如果marker已定义,则在将位置或限制调整为小于标记的值时将丢弃标记。如果标记没有定义,那么调用reset()方法会导致抛出InvalidMarkException。
#不变性
mark、position、limit和capacity值不变:
新创建的缓冲区的position总是为零,mark为undefined。初始limit可以是零,也可以是其他一些值,这取决于缓冲区的类型和构造方式。新分配的缓冲区中的每个元素都被初始化为0。
clear、flip、rewind
除了postion、limit和capacity以及mark和reset的方法,这个类还定义了对缓冲区的以下操作:
#clear
用于清空数据,其实就是将指针恢复初始值
将 limit 设置为 capacity ,position 设置为 0
#flip
反转,用于读写切换
将 limit 设置为当前的 position 将 position 设置为 0
#rewind
用于为重新读取数据做准备
将 position 设置为 0 ,limit 不变,mark 丢弃
线程安全性
对于多个并发线程使用缓冲区是不安全的。如果一个缓冲区被多个线程使用,那么对缓冲区的访问应该由synchronization进行控制
首选我们来看一下由于创建Buffer的静态方法 allocate
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
/**
* 分配一个新的Buffer
* 默认position为0,limit为我们传入的capacity,到目前位置没有定义mark,
* 每个元素都将被初始化为0,(limit和capacity不包括).
* 数据其实存放在数组中,数组的偏移量为0
* 返回的对象是 HeapByteBuffer(堆字节缓冲区)。
*/
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
我们发现它返回的对象不是自身 ByteBuffer 而是 HeapByteBuffer,因为 ByteBuffer 是一个abstract类。抽象类不能被实例化
哈哈哈这时候会不会有人说你这里不是用的 ByteBuffer类型的引用嘛,为什么还说还是不能被实例化呢? 这时候就需要去了解一下面向接口的编程了。
让我们来看一下 HeapByteBuffer
/**
* read/write HeapByteBuffer. 是一个可读可写的Buffer
* HeapByteBuffer继承自ByteBuffer,并且没有权限修饰符(不能通过new关键字实例化), * 所以这里是通过return返回的
*/
class HeapByteBuffer
extends ByteBuffer{
}
HeapByteBuffer 在构造方法中通过super调用父类(ByteBuffer)的构造,然后 ByteBuffer又通过super调用父类(Buffer)的构造 -> 最终就是完成一些列的初始化。 很容易理解看一遍就懂了。
说了这么多,那Buffer是如何放至元素的呢?
//当我们看完put的逻辑就知道Buffer是如何放置元素的啦。。。
//注意是HeapByteBuffer中的put,因为我们实际使用的是HeapByteBuffer
//put调用
byteBuffer.put(new byte[1]);
----------------------------------分割线---------------------------------
//put源码
//hb是一个数组,里面存放的是Buffer的数据
//
public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
//nextPutIndex()方法和ix方法就是找位置 就像我们平常使用的 arr[index] = 0
final int nextPutIndex() {
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
//offset 偏移量
protected int ix(int i) {
return i + offset;
}
图解
Mark
Reset
Clear
Flip
我们来看一段程序
光看源码还是不太明白怎么办?下面我们来看一段程序
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("2021_fc.txt");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
fileChannel.read(byteBuffer);
byteBuffer.flip();
while (byteBuffer.remaining()>0){
byte b = byteBuffer.get();
System.out.println("Character: " + (char)b);
}
fileInputStream.close();
}
图解
fileChannel.read(byteBuffer); 往buffer中读取数据,假设我们的2021_fc.txt中只有五个字节。那么如图 ->
byteBuffer.flip(); 调用flip,进行数据翻转。各属性指向如图 ->
将limt指向position,将position置为0,而Capacity不会发生改变,永远为初始化时的容量大小。
那么如果不调用flip()方法,会发生什么呢?
我的猜想是position会从索引5开始继续往后写,但是因为已经没有数据了所以都是为空
让我们来实际看看程序的输出是不是如我猜想的那样吧 ->
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("2021_fc.txt");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
fileChannel.read(byteBuffer);
System.out.println("flip之前 limit值:" + byteBuffer.limit());
//byteBuffer.flip();
System.out.println("flop之后 limit值:" + byteBuffer.limit());
while (byteBuffer.remaining()>0){
System.out.println("limit: " + byteBuffer.limit());
System.out.println("position: " + byteBuffer.position());
System.out.println("capacity: " + byteBuffer.capacity());
byte b = byteBuffer.get();
System.out.println("Character: " + (char)b);
}
fileInputStream.close();
}
--------------------------------输出结果---------------------------------
limit: 10
position: 5
capacity: 10
Character:
......
limit: 10
position: 9
capacity: 10
Character:
正如我们猜想的那样, 调用flip的输出结果 ->
limit: 5
position: 0
capacity: 10
Character: A
limit: 5
position: 1
capacity: 10
Character: B
limit: 5
position: 2
capacity: 10
Character: C
limit: 5
position: 3
capacity: 10
Character: D
limit: 5
position: 4
capacity: 10
Character: E
Rewind