Java NIO (New IO)是从Java1.4版本开始引入的一个新的IO API,可以替代次奥准的Java IO API。NIO支持面向缓冲区的,基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。这篇文章是根据课件做的学习笔记,比较长。可以根据选择查看。
比较NIO与IO
特性对比
IO模型 | IO | NIO |
---|---|---|
通信 | 面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
处理 | 阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
触发 | (无) | 选择器(Selectors) |
面向流与面向缓冲区的区别以及对通道与缓冲区的理解
-
面向流是单向的,文件与程序之间建立数据流,输入流和输出流都需要建立不同的“管道”。抽象的理解为自来水管和下水道吧,水就是传输的数据。
-
面向缓冲区,文件与程序之间建立通道,里面存在缓冲区。抽象的理解可以把通道认为是铁路,缓冲区认为是一辆火车,而载着的货物也就是所要传输的数据了。
-
简单认为:通道负责传输,缓冲区负责存储。
缓冲区(Buffer)
Buffer在Java NIO 中负责数据的存取,缓冲区就是数组,用于存储不同数据类型的数据。
缓冲区类型
根据数据类型的不同(boolean除外),提供了相应类型的缓冲区。
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- 上述缓冲区的管理方式几乎一致,通过allocate()获取缓冲区。
- ByteBuffer最为常用
缓冲区存取数据的两个核心方法
-
put():存入数据到缓冲区中
-
get():获取缓冲区中的数据
缓冲区的四个核心属性
-
capacity: 容量,表示缓冲区中最大存储数据的容量,一但声明不能改变。(因为底层是数组,数组一但被创建就不能被改变)
-
limit: 界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)
-
position: 位置,表示缓冲区中正在操作数据的位置
- position <= limit <= capacity
-
mark:标记,表示记录当前position的位置,可以通过reset()恢复到mark的位置。
几个常用方法
- allocate():分配缓冲区:
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
- put():将数据存入缓冲区
String str = "这是一个测试数据";
byteBuffer.put(str.getBytes());
-
flip():切换到读取数据的模式
-
get():读取数据
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(new String(bytes,0,bytes.length));
-
rewind():重复读,使position归0
-
clear():清空缓冲区,但是缓冲区中的数据依然存在,只是处于一种“被遗忘“的状态。只是不知道位置界限等,读取会有困难。
-
mark():标记。mark会记录当前的position,limit,capacity
-
reset():position,limit,capacity恢复到mark记录的位置
直接缓冲区与非直接缓冲区
-
非直接缓冲区:通过
allocate()
方法分配缓冲区,将缓冲区建立在JVM的内存
中。在每次调用基础操作系统的一个本机IO之前或者之后
,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易
,但是占用JVM内存开销
,处理过程中有复制操作。 -
非直接缓冲区的写入步骤:
-
创建一个临时的ByteBuffer对象。
-
将非直接缓冲区的内容复制到临时缓冲中。
-
使用临时缓冲区执行低层次I/O操作。
-
临时缓冲区对象离开作用域,并最终成为被回收的无用数据。
-
直接缓冲区:通过
allocateDirect()
方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。 -
直接缓冲区在JVM内存外开辟内存,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会避免将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在物理内存内,会少一次复制过程,如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能。虽然直接缓冲区使JVM可以进行高效的I/O操作,但它使用的内存是操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓冲区要更大的开销
- 观察源码
- allocate():
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
进入到 HeapByteBuffer()中可以看到:
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
可以看出直接在堆内存中开辟空间,也就是数组。
- allocateDriect():
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
进入到DirectByteBuffer()中可以看到:
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
由 VM.isDirectMemoryPageAligned();
可以看出直接调用了内存页,让操作系统开辟缓存空间。
通道
通道(Channel)表示IO源与目标打开的连接。Channel类似于传统的”流“,只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。
- Channel是一个独立的处理器,专门用于IO操作,附属于CPU。
- 在提出IO请求的时候,CPU不需要进行干预,也就提高了效率。
作用
用于源节点与目标节点的连接。在Java NIO中负责缓冲区中数据的传输。Channel本身并不存储数据,因此需要配合Buffer一起使用
主要实现类
java.nio.channels.Channel接口:
- 用于本地数据传输:
|-- FileChannel
- 用于网络数据传输:
|-- SocketChannel
|-- ServerSocketChannel
|-- DatagramChannel
获取通道
- Java 针对支持通道的类提供了一个
getChannel()
方法。
-
本地IO操作
- FileInputStream/File Output Stream
- RandomAccessFile
-
网络IO
- Socket
- ServerSocket
- DatagramSocket
- 在JDK1.7中的NIO.2 针对各个通道提供了静态方法 open();
- 在JDK1.7中的NIO.2 的Files工具类的 newByteChannel();
利用通道完成文件的复制
-
非直接缓冲区
@Test public void testChannel1(){ FileInputStream inputStream = null; FileOutputStream outputStream = null; FileChannel inputChannel = null; FileChannel outputChannel = null; try { inputStream = new FileInputStream(new File("H:\\img\\1.jpg")); outputStream = new FileOutputStream(new File("H:\\img\\2.jpg")); // 获取通道 inputChannel = inputStream.getChannel(); outputChannel = outputStream.getChannel(); // 分配缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 将通道中数据存入缓冲区 while(inputChannel.read(byteBuffer) != -1){ // 切换成读取数据的模式 byteBuffer.flip(); //缓冲区中数据写到通道中区 outputChannel.write(byteBuffer); // 清空缓冲区 byteBuffer.clear(); } System.out.println("读写成功"); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { //关闭通道 ...略(如果不为null,执行close方法) System.out.println("数据关闭成功"); } }
-
直接缓冲区
@Test public void channelTest2() throws IOException { FileChannel inputChannel = FileChannel.open(Paths.get("H:\\img\\9.jpg"), StandardOpenOption.READ); FileChannel outputChannel = FileChannel.open(Paths.get("H:\\img\\10.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE); // 内存映射文件 MappedByteBuffer inputBuffer = inputChannel.map(FileChannel.MapMode.READ_ONLY,0,inputChannel.size()); MappedByteBuffer outputBuffer = outputChannel.map(FileChannel.MapMode.READ_WRITE,0,inputChannel.size()); byte [] bytes = new byte[inputBuffer.limit()]; inputBuffer.get(bytes); outputBuffer.put(bytes); inputChannel.close(); outputChannel.close(); }
- 使用直接缓冲区对文件的存储性能会有极大的提升,但是直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
-
通道之间的数据传输(也是利用的直接缓冲器的方式)
- transferFrom();
- transferTo();
@Test public void ChannelTest3() throws IOException { FileChannel inputChannel = FileChannel.open(Paths.get("H:\\img\\12.jpg"), StandardOpenOption.READ); FileChannel outputChannel = FileChannel.open(Paths.get("H:\\img\\haha.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE); // 两种方式都行 //inputChannel.transferTo(0,inputChannel.size(),outputChannel); outputChannel.transferFrom(inputChannel,0,inputChannel.size()); inputChannel.close(); outputChannel.close(); }
分散(Scatter)与聚集(Gather)
分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中
聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中
分散读取
]
聚集写入
读写案例代码
@Test
public void test1() throws IOException {
// rw代表 读写模式
RandomAccessFile file = new RandomAccessFile("G:\\notes\\nio\\01_understand\\学习使用NIO.md","rw");
FileChannel channel = file.getChannel();
// 分配制定缓冲区
ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024*2);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024*6);
ByteBuffer byteBuffer3 = ByteBuffer.allocate(1024*5);
// 分散读取
ByteBuffer[] buffers= {byteBuffer1,byteBuffer2,byteBuffer3};
channel.read(buffers);
for (ByteBuffer buffer : buffers) {
buffer.flip();
}
// 聚集写入
RandomAccessFile file2 = new RandomAccessFile("C:\\Users\\admin\\Desktop\\nio.txt","rw");
// 获取 通道
FileChannel channel2 = file2.getChannel();
channel2.write(buffers);
channel.close();
channel2.close();
}
字符集Charset
设置字符集,解决乱码问题
编码:字符串->字节数组
解码:字节数组->字符串
思路
-
用Charset.forName(String)构造一个编码器或解码器,利用编码器和解码器来对CharBuffer编码,对ByteBuffer解码。
-
需要注意的是,在对CharBuffer编码之前、对ByteBuffer解码之前,请记得对CharBuffer、ByteBuffer进行flip()切换到读模式。
-
如果编码和解码的格式不同,则会出现乱码。
实现
@Test
public void CharacterEncodingTest() throws CharacterCodingException {
Charset charset = Charset.forName("utf-8");
Charset charset1 = Charset.forName("gbk");
// 获取编码器 utf-8
CharsetEncoder encoder = charset.newEncoder();
// 获得解码器 gbk
CharsetDecoder decoder = charset1.newDecoder();
CharBuffer buffer = CharBuffer.allocate(1024);
buffer.put("绝不敷衍,从不懈怠!");
buffer.flip();
// 编码
ByteBuffer byteBuffer = encoder.encode(buffer);
for (int i = 0; i < 20; i++) {
System.out.println(byteBuffer.get());
}
// 解码
byteBuffer.flip();
CharBuffer charBuffer = decoder.decode(byteBuffer);
System.out.println(charBuffer.toString());
}
-
在for循环中使用过到了ByteBuffer的get()方法。一开始习惯性的在get()方法里加上了变量i随即出现了问题,无法取得数据。注释代码
byteBuffer.flip();
之后可以执行。当直接使用get()方法时,不加byteBuffer.flip();
则会报错。所以就来区别一下ByteBuffer里的get();与get(int index);的区别。 -
查看get();方法源码:
/** * Relative <i>get</i> method. Reads the byte at this buffer's * current position, and then increments the position. * @return The byte at the buffer's current position * * @throws BufferUnderflowException * If the buffer's current position is not smaller than its limit */ public abstract byte get();
可以看出返回的值是“ The byte at the buffer’s current position”,就是返回缓冲区当前位置的字节。"then increments the position"也说明了返回字节之后,position会自动加1,也就是指向下一字节。
-
上述情况如果是get(index),则是下面的方法:
/** * Absolute <i>get</i> method. Reads the byte at the given * index. * @param index * The index from which the byte will be read * * @return The byte at the given index * * @throws IndexOutOfBoundsException * If <tt>index</tt> is negative * or not smaller than the buffer's limit */ public abstract byte get(int index);
由“The byte at the given index”可以知道返回的是给定索引处的字节。position并未移动。如果之后再执行flip();操作则读取不到任何数据。原因接着往下看。
-
再来看一看 flip();方法源码:
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
注意:
limit=position
,如果使用get(index);的方法,则执行完position = 0,所以limit也会变成0,之后无法读取数据。
网络阻塞IO与非阻塞IO
了解
- 传统IO是阻塞式的,也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
- NIO是非阻塞式的,当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO操作,所以单独的线程可以管理多个输入和输出通道。因此, NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
阻塞模式与非阻塞模式
-
传统阻塞IO方式:客户端向服务器端发送请求,服务器端便开始进行监听客户端的数据是否传过来。这时候客户端在准备自己的数据,而服务器端就需要干等着。即使服务器端是多线程的,但有时一味增加线程数,只会让阻塞的线程越来越多。
-
NIO的非阻塞方式:将用于传输的通道全部注册到选择器上。
选择器的作用是监控这些通道的IO状况(读,写,连接,接收数据的情况等状况)。
-
选择器与通道之间的联系:
- 通道注册到选择器上
- 选择器监控通道
当某一通道,某一个事件就绪之后,选择器才会将这个通道分配到服务器端的一个或多个线程上,再继续运行。例如客户端需要发送数据给服务器端,只当客户端所有的数据都准备完毕后,选择器才会将这个注册的通道分配到服务器端的一个或多个线程上。而在客户端准备数据的这段时间,服务器端的线程可以执行别的任务。
使用NIO完成网络通信的三个核心
-
通道(Channel):负责连接
java.mio.channels.Channel 接口: |-- SelectableChannel |--SocketChannel |--ServerSocketChannel |--DatagramChannel |--Pipe.SinkChannel |--Pipe.sourceChannel
-
缓冲区(Buffer):负责数据的存取
-
选择器(Select):是SelectableChannel的多路复用器。用于监控SelectableChannel的IO状况。
非阻塞模式完成客户端向服务器端传输数据
@Test
public void client() throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",7498));
// 切换成非 阻塞模式
socketChannel.configureBlocking(false);
FileChannel inputChannel = FileChannel.open(Paths.get("G:\\notes\\nio\\01_简介\\学习使用NIO.md"), StandardOpenOption.READ);
ByteBuffer clientBuffer = ByteBuffer.allocate(1024);
while (inputChannel.read(clientBuffer) != -1){
clientBuffer.flip();
socketChannel.write(clientBuffer);
clientBuffer.clear();
}
socketChannel.close();
inputChannel.close();
}
@Test
public void server() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 非阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(7498));
FileChannel outputChannel = FileChannel.open(Paths.get("C:\\Users\\admin\\Desktop\\test.md"),StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 选择器
Selector selector = Selector.open();
// 将通道注册到选择器上,并制定监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 轮巡式获得选择器里的已经准备就绪的事件
while (selector.select() > 0 ){
// 获取已经就绪的监听事件
Iterator<SelectionKey> selectorIterator = selector.selectedKeys().iterator();
// 迭代获取
while (selectorIterator.hasNext()){
// 获取准备就绪的事件
SelectionKey key = selectorIterator.next();
SocketChannel socketChannel = null;
// 判断是什么事件
if (key.isAcceptable()){
// 或接受就绪,,则获取客户端连接
socketChannel = serverSocketChannel.accept();
//切换非阻塞方式
socketChannel.configureBlocking(false);
// 注册到选择器上
socketChannel.register(selector,SelectionKey.OP_READ);
} else if (key.isReadable()){
// 获取读就绪通道
SocketChannel readChannel = (SocketChannel) key.channel();
readChannel.configureBlocking(false);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int len = 0;
while ( (len = readChannel.read(readBuffer)) != -1){
readBuffer.flip();
System.out.println(new String(readBuffer.array(),0,len));
outputChannel.write(readBuffer);
readBuffer.clear();
}
readChannel.close();
outputChannel.close();
}
}
// 取消选择键
selectorIterator.remove();
/**
* 这里就像评论中所说的那样,
* “serverSocketChannel.close();不用关闭的,这是服务器端”
* 没有及时更正,抱歉
*/
// serverSocketChannel.close();
}
}
-
先启动服务器端,再启动客户端。
-
按理说同时可以启动多个客户端,但是我的测试时只能正确启动一次,第二次启动在建立SocketChannel时便报错。错误信息如下:
java.net.ConnectException: Connection refused: connect
。 这里出现错误的原因就是评论里所说的。
DatagramChannel
这个与上面的非常相似,所以这里只给一个实现的代码案例:
@Test
public void send() throws IOException {
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
String str = "随便写写,测试一下";
ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
sendBuffer.put(str.getBytes());
sendBuffer.flip();
datagramChannel.send(sendBuffer,new InetSocketAddress("127.0.0.1",7498));
sendBuffer.clear();
datagramChannel.close();
}
@Test
public void recive() throws IOException{
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.bind(new InetSocketAddress(7498));
Selector selector = Selector.open();
datagramChannel.register(selector,SelectionKey.OP_READ);
while (selector.select() > 0){
Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
while (selectionKeyIterator.hasNext()){
SelectionKey key = selectionKeyIterator.next();
if (key.isReadable()){
ByteBuffer reciveBuffer = ByteBuffer.allocate(1024);
datagramChannel.receive(reciveBuffer);
reciveBuffer.flip();
System.out.println(new String(reciveBuffer.array(),0,reciveBuffer.limit()));
reciveBuffer.clear();
}
}
selectionKeyIterator.remove();
}
datagramChannel.close();
}
管道(Pipe)
Java NIO 管道是两个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
代码示例
@Test
public void test() throws IOException {
// 获取管道
Pipe pipe = Pipe.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将缓冲区中数据写入管道
Pipe.SinkChannel sinkChannel = pipe.sink();
buffer.put("要死了要死了要死了,,,救救孩子吧".getBytes());
buffer.flip();
sinkChannel.write(buffer);
// 为了省事,就不写两个线程了
// 读取缓冲区中数据
Pipe.SourceChannel sourceChannel = pipe.source();
buffer.flip();
System.out.println(new String(buffer.array(),0,sourceChannel.read(buffer)));
sinkChannel.close();
sourceChannel.close();
}
最后
初步跟着视频把笔记整理完了。当然还有很多的不足,遗漏。这里加两个文章连接吧,一个是讲解阻塞IO与非阻塞IO的,一个是阿里巴巴云栖社区的关于NIO的文章。