一、IO和NIO的区别
IO是传统的面向流的阻塞IO,而NIO是面向缓冲区的非阻塞式IO。在NIO中使用了一个线程来作为Selectors-选择器,来管理多个输入通道,即在使用时只需要将通道注册到选择器中,即可处理输入的通道和选择已经准备好的通道进行管理。
二、Buffer缓冲区与传统IO流
· 传统IO流是对字节数组的流动,单向的输入流和输出流,即面对流的传输。
· NIO本质上也是一个输入输出流。但是NIO中使用了Buffer缓冲区,会将需要传输的数据存放到缓冲区中再结合通道来进行输入与输出,即面对缓冲区的传输。
· NIO的核心也在于缓冲区与通道,简单来说,缓冲区用于存储需要传输的数据,通用用于传输时的连接。
三、学习Buffer缓冲区
上述已提到,Buffer在NIO中主要负责数据的存储。并且根据数据类型的不同,有着不一样的数据缓冲区。
查看Buffer类的引用可以看到,有各种各样的子类Buffer代表着可以存储不同数据的缓冲区,如:ByteBuffer、FloatBuffer、DoubleBuffer等。
· 缓冲区核心属性值
在缓冲区父类Buffer中存在这些核心的参数用于缓冲区取值与放置值的操作。
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
(1)capacity-容量,代表缓冲区中最大存储数据容量,一旦创建不允许改变。
(2)limit-界限,代表缓冲区中可以操作数据的大小。在源码中可以看到这样一个注释即limit<=capacity,再结合limit的含义,表示只能操作0~limit之间的数据,超过limit后的数据是不能进行操作的。
(3)position-正在操作的位置,代表缓冲区中正在操作的位置,即position<=limit<=capacity。
(4)mark-辅助标记,可以记录当前position的记录。
· 创建缓冲区
在使用缓冲区存储数据之前,需要创建缓存区,可以使用allocate方法来创建缓冲区并分配大小。
即:创建和分配了一个1024字节的Byte缓冲区。
ByteBuffer buffer = ByteBuffer.allocate(1024);
观察此时的capacity、limit、position。如下图所示:
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}102410240
刚创建缓冲区后三者属性关系如下图所示:
· 缓冲区中放入值
可以使用put,来往缓冲区中放入值。
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcde".getBytes());
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}1024
10245
往缓冲区中放入值后三者属性关系如下图所示:
· 缓冲区中取值
上述的缓冲区可以看到position为5,并且limit和capacity都为1024,代表着还可以写数据从5~limit,此时的缓冲区为写的模式。如果想要从缓冲区中取值,需要调用flip来切换模式。
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcde".getBytes());
//切换模式
buffer.flip();
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}102450
切换模式后的缓冲区三者属性关系如下图所示:
切换为读模式后,可以看到limit和position都切换了位置,表示在读数据模式中可以操作的数组大小为0~5。
使用get方法获取缓冲区中的数据。
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcde".getBytes());
//切换模式
buffer.flip();
//读取全部缓存区数据或者读取一个数据
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
//buffer.get();
System.out.println(new String(bytes, 0, bytes.length));
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}abcde
1024
55
读缓冲区数据后的三者属性关系如下图所示:
此时的position和limit已经相等,如果再继续使用get获取数据就会抛出异常。
后续如果还需要从0开始读取,可以使用flip方法或者rewind方法。
· 缓冲区回到最初状态
可以使用buffer.clear来清空缓冲区,回到最初状态。
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcde".getBytes());
//切换模式
buffer.flip();
//读取全部缓存区数据或者读取一个数据
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
buffer.clear();
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
System.out.println((char) buffer.get());
}102410240
a
上述运行结果可以看到,虽然缓冲区回到了最初状态,但是缓冲区中还存在着之前存放的值,并且可以被读取到。
因此在不知道之前缓冲区存放多少数据的情况,即不知道position的情况时,可以使用mark属性,记录一个position的快照,并且快速回到position快照的位置。
· 缓冲区mark标记
mark标记时,主要使用mark方法来标记position当时的快照位置,然后使用reset方法使position回到mark标记的位置。
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("abcde".getBytes());
//切换模式
buffer.flip();
//读取全部缓冲区数据或者读取一个数据
buffer.mark();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println(buffer.position());
buffer.reset();
System.out.println(buffer.position());
}5
0
上述代码中的运行结果显示,在读取时先标记当时position的位置,mark此时为0。进行数据读取后,position指向了5,最后使用reset使position回到了快照的位置0。
操作mark标记前后四者属性关系如下图:
四、非直接缓冲区
文章上述所示的使用buffer.allocate方法创建和分配的缓冲区都为非直接缓冲区,他是将缓冲区建立在JVM内存中。
非直接缓冲区工作图如下所示:
读操作时都是会将数据copy到JVM中与应用程序交互或者写操作时会将数据copy到JVM中与cpu内存、磁盘进行交互。
非直接缓冲区的IO操作时,都会将数据copy到JVM中与磁盘或程序进行交互,如果认为不需要有copy这一步操作的可以使用直接缓冲区来进行IO操作。
观察allocate源码,操作的是堆内存:
五、直接缓冲区
直接缓冲区内存工作图如下所示:
程序的IO操作可以直接与物理内存进行操作,不需要有copy的一步来减少数据复制的开销,提高IO效率。这里的物理内存是申请的一份空间,主要是作为磁盘和程序数据传输的一个中间媒介。
直接缓冲区存在的问题:
(1)JVM外空间分配比JVM空间分配更加耗时、有着更多的消耗。
(2)数据写入物理内存缓冲区中,物理内存并不会立即向磁盘同步这些数据,而写入磁盘只能由操作系统决定什么时候同步到磁盘中,并且应该程序无法干涉这些数据。
对于一些大数据,由于在JVM中这些大对象会因为内存空间不足而引起full gc导致系统卡顿时,对这些大数据的操作可以使用直接缓冲区操作。
· 如何创建直接缓冲区
可以通过allocateDirect方法来创建直接缓冲区。
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
System.out.println(buffer.capacity());
System.out.println(buffer.limit());
System.out.println(buffer.position());
}
底层代码中调用的是内存页。