IO与NIO的区别:注意:两者都是同步的
同步时:应用程序(代码)会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪;或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据。
异步是,则所有的IO读写操作交给操作系统处理,与我们的应用程序没有直接关系,我们的程序不需要关系IO读写,当操作系统完成IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据即可。
1、面向流与面向缓冲
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据通过管道读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
2、阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
非阻塞主要体现在,原来的io接受请求后一直读写知道完成,但是nio会轮询相关状态,当状态可用才分配线程干活,分配之前它是不需要线程的,只需要一个线程来轮询所有管道。
3、选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
我们先从ByteBuffer的allocate分配字节数组看起
我们发现这个方法接受一个capacity参数,也就是容量参数
返回了一个HeapByteBuffer(capacity,capacity)对象,我们看下这个类
他通过传入的容量值够早了一个
mark为-1 pos为0 lim为cap 大小为cap的字节数组 偏移量为0
可以看出分配的buffer是在heap区域的, 它调用了父类的构造方法,它的父类是ByteBuffer
我们看下:
这里主要完成了数组单位的的赋值
我们先看下它的父类Buffer的构造方法:
这里进行判断传入的值是否符合要求
我们看下传入的参数的方法
我们发现里面有两个方法是limit和position
这两个方法在这里的作用就是判断传入的limit值和position的值是否合法
总结这个构造方法就是:返回一个在堆内存分配的相应类型的缓冲区,并且进行了一系列的传入参数合法判断。
剩下的看这位博主的就好了
https://blog.csdn.net/u010412719/article/details/52775637
这里要说一个问题,看网上最常见的图
这个图真的太容易误导人了,首先我们知道一个缓存空间底层是通过字节数组实现的,那么这个字节数组创建时相当于这样的语句new ByteBuffer【capacity】,那么这个数组长度为capacity,但是索引不应该是capacity。,因为索引无法达到这个长度,换句话说这个图的capacity改为capacity-1才对
那么应该这么定义:position的值是下一个要被写入或者被读的空间,它的值就是所有被读取或者被写入的长度(从0开始)
limit的值:是限制空间(能读或者能写多少字节)的长度
capacity的值:是总长度,不应该讲为索引
那么我们看下它的读取和写入是如何实现的就能更深的理解这个东西了
put(int i)方法介绍
下面来看IntBuffer类中的put方法
public abstract IntBuffer put(int i);
public abstract int get(int index);
在IntBuffer类中put、get方法都是抽象的。
有了上面allocate方法的过程分析,我们知道IntBuffer buffer = IntBuffer.allocate(cap)
中的buffer实际上是父类的引用指向的是子类的对象。当我们使用buffer.put(value)/buffer.get().
实际上是调用子类HeapIntBuffer类中的put/get方法,这就是多态。在Java中相当重要的一个特征。
HeapIntBuffer类中put方法的实现如下:
public IntBuffer put(int x) {
hb[ix(nextPutIndex())] = x;
return this;
}
put方法实现的思想就是:将值存储在position位置即可。
这里涉及到两个新的函数,分别为:
1、nextPutIndex(),函数功能:简单来说就是返回下一个要写入元素的索引位置(即当前position值),并将position进行加一操作。
2、ix(int i):偏移offset个位置
这两个函数的实现如下:
//函数功能:首先检查当前的position是否小于limit,如果小于则存储,否则抛异常。
final int nextPutIndex() { // package-private
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
//函数功能:偏移offset个位置
protected int ix(int i) {
return i + offset;
}
我们主要看着两个方法:如果从一开始读或者写的话那么偏移量为0,我们只看第一个方法:
判断条件为:position>=limit 这里用的是>=不是>就很好说明了limit只是一个长度
如果position等于了limit,那么此时position已经超出界限了,不能读写
注意:limit和capacity都是长度而不是索引,我们每次得到position时会++,这样当position达到最终写完长度的索引时,它刚好还加了1,那么我们切换读模式的时候直接将position赋值给limit就行了。
3、get()方法介绍
看完了put方法,接下来来看下get方法
函数的功能:取得Buffer中position位置所指向的元素。
//函数功能:获取buffer中position所指向的元素。
public int get() {
return hb[ix(nextGetIndex())];
}
//函数功能:获取buffer中索引为i位置的元素
public int get(int i) {
return hb[ix(checkIndex(i))];
}
在get()方法中也涉及到两个另外的函数:
1、nextGetIndex(),函数功能:返回下一个读取的元素的索引位置(即position值)
2、ix(int i)
final int nextGetIndex() { // package-private
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
//将position向右移动nb个位置,这个函数目前还没有看见在哪里得到的应用
final int nextGetIndex(int nb) { // package-private
if (limit - position < nb)
throw new BufferUnderflowException();
int p = position;
position += nb;
return p;
}
这个方法和哪个方法一样。也是操作position