1.简介
Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
1.1.IO和NIO的区别:
IO | NIO |
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(NonBlocking IO) |
1.1.1 面向流和面向缓冲区:
传统的IO再进行数据传输的时候,会根据输入和输出的不同分别建立不同的链接,传输的数据会转化成数据流的形式在链接中传输,就像自来水流在自来水管中传输,电流在电线中传输一样。
面向缓冲区的NIO在传输数据的时候,会在输入和输出之间建立通道,然后将数据放入缓冲区,缓冲区通过通道在输入和输出之间传输。这时候的传输模型就像快递一样,我们只需要把快递放到快递投递点,如何传输就只需要交给快递员传输就可以了。
2.通道与缓冲区
在JAVA NIO中的通道和缓冲区是什么?
通道表示程序跟IO设备之间的链接,缓冲区分为直接与非直接缓冲区,直接缓冲区是在操作系统内存中开辟的一片区域(详情见JVM内存管理的直接内存);二、非直接缓冲区是JVM分配的一块内存。
非直接缓冲区:通过allocate()方法获取的缓冲区都是非直接缓冲区。这些缓冲区是建立在JVM堆内存之中的。
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
// 在堆内存中开辟空间
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) { // package-private
// new byte[cap] 创建数组,在堆内存中开辟空间
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
图中所示:通过非直接缓冲区,想要将数据写入到物理磁盘中,或者是从物理磁盘读取数据。都需要经过JVM和操作系统,数据在两个地址空间中传输时,会copy一份保存在对方的空间中。所以费直接缓冲区的读取效率较低 。
直接缓冲区:只有ByteBuffer可以获得直接缓冲区,通过allocateDirect()获取的缓冲区为直接缓冲区,这些缓冲区是建立在物理内存(直接内存)之中的。
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) { // package-private
...
// 申请物理内存
boolean pa = VM.isDirectMemoryPageAligned();
...
}
如图所示 直接缓冲区通过在操作系统和JVM之间创建物理内存映射文件加快缓冲区数据读/写入物理磁盘的速度。放到物理内存映射文件中的数据就不归应用程序控制了,操作系统会自动将物理内存映射文件中的数据写入到物理内存中。
若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
简而言之,通道负责传输,缓冲区负责存储。
2.1.缓冲区类型
Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下Buffer 常用子类
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
各种类型的缓冲区中,都有一个对应类型的数组,比如ByteBuffer对应的byte[] buffer。
缓冲区的获取:
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
2.2.核心属性
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
capacity:缓冲区的容量。通过构造函数赋予,一旦设置,无法更改
limit:缓冲区的界限。位于limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量
position:下一个读写位置的索引(类似PC)。缓冲区的位置不能为负,并且不能大于limit
mark:记录当前position的值。position被改变后,可以通过调用reset() 方法恢复到mark的位置。
位置关系:mark <= position <= limit <= capacity
2.3.核心方法:
put()方法:
put()方法可以将一个数据放入到缓冲区中。
进行该操作后,postition的值会+1,指向下一个可以放入的位置。capacity = limit ,为缓冲区容量的值。
flip()方法
flip()方法会切换对缓冲区的操作模式,由写->读 / 读->写
进行该操作后
如果是写模式->读模式,position = 0 , limit 指向最后一个元素的下一个位置,capacity不变;
如果是读->写,则恢复为put()方法中的值。
get()方法
get()方法会读取缓冲区中的一个值
进行该操作后,position会+1,如果超过了limit则会抛出异常;
rewind()方法
该方法只能在读模式下使用
rewind()方法后,会恢复position、limit和capacity的值,变为进行get()前的值
clean()方法
clean()方法会将缓冲区中的各个属性恢复为最初的状态,position = 0, capacity = limit
此时缓冲区的数据依然存在,处于“被遗忘”状态,下次进行写操作时会覆盖这些数据。
mark()和reset()方法
mark()方法会将postion的值保存到mark属性中
reset()方法会将position的值改为mark中保存的值
2.4.通道
Channel由java.nio.channels 包定义的。Channel 表示IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过Channel 本身不能直接访问数据,Channel 只能与Buffer 进行交互。
2.4.1 java channel
本地文件IO
FileChannel
网络IO
SocketChanel、ServerSocketChannel:用于TCP传输
DatagramChannel:用于UDP传输
2.4.2.获得通道的方法
1.getChannel()方法
支持该方法的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
public class Demo2 {
public static void main(String[] args) throws IOException {
// 本地通道
FileInputStream fileInputStream = new FileInputStream("");
FileChannel channel1 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("");
FileChannel channel2 = fileOutputStream.getChannel();
// 网络通道
Socket socket = new Socket();
SocketChannel channel3 = socket.getChannel();
ServerSocket serverSocket = new ServerSocket();
ServerSocketChannel channel4 = serverSocket.getChannel();
DatagramSocket datagramSocket = new DatagramSocket();
DatagramChannel channel5 = datagramSocket.getChannel();
// 最后要关闭通道
}
}
2.open方法
public static void main(String[] args) throws IOException {
FileChannel open = FileChannel.open(Paths.get(""));
SocketChannel open1 = SocketChannel.open();
...
}
2.5.分散和聚集
分散读取:
分散读取(Scattering Reads)是指从Channel 中读取的数据“分散”到多个Buffer 中;
注意:按照缓冲区的顺序,从Channel 中读取的数据依次将 Buffer 填满。
聚集写入
聚集写入(Gathering Writes)是指将多个Buffer 中的数据“聚集”到Channel;
按照缓冲区的顺序,写入position 和limit 之间的数据到Channel。
3.非阻塞式网络通信
3.1阻塞式网络通信
传统的IO 流都是阻塞式的。也就是说,当一个线程调用read() 或write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。
因此,在完成网络通信进行IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
也就是说,服务器在等待IO准备就绪的期间,线程处于阻塞状态,若为单线程,等待期间CPU未执行任何任务,效率降低。
所以需要开启多个线程,当某些线程因为等待IO准备就绪时,CPU可以去执行其他线程中的任务。但是线程的创建、切换与销毁的开销也是不小的。当大量的任务到来时,服务器性能也急剧下降。
3.2非阻塞式网络通信
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞IO 的空闲时间用于在其他通道上执行IO 操作,所以单独的线程可以管理多个输入和输出通道。
因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。