一、IO与NIO的区别
1、传统IO
面向流(输入输出流)、基于管道单向运输、是一个阻塞型IO。
2、NIO
面向缓冲区、基于通道双向传输、是非阻塞的。
当我们在“文件、磁盘、网络” 与程序之间传输数据的时候,IO 通过 一个“管道 ”连接两者,然后通过建立“输入流”或者“输出流” 对数据进行输入和输出的操作,所以是单向的。
而NIO通过连接一个“通道(Channel)” ,在此通道里建立一个“缓冲区(Buffer)”,把数据存放在这个缓冲区,又或者说,这个“缓冲区”相当于 数据传输两方的“媒人”,是双向关系的。
二、NIO缓冲区的存取
(一)概念:
缓冲区在Java中负责数据的存取,底层由数组实现,用于存储不同数据类型的数据。根据数据类型的不同,提供了不同数据类型的缓冲区(除了boolean类型):
ByteBuffer、IntBuffer、CharBuffer、ShortBuffer、、、等等。
上述不同数据类型的缓冲区由 方法“allocate(指定大小)” 获取,如:
//创建一个容量为1024字节的byte缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
(二)存储数据的核心方法
|----------get() ; //获取缓冲区中的数据
|----------put() ;//向缓冲区中输入数据
(三)缓冲区中的四个核心属性
Buffer.java 源 码中 有四个属性:
|----------position : 位置, 表示正在操作的数据的位置。
|----------limit : 界限,表示允许操作的缓冲区界限,limit后的位置不能|----------被读写。
|----------capacity : 容量,表示数组的容量,也就是缓冲区的大小,一旦创建,不可以更改。
|----------mark : 标记,用于标记position的位置。
(:这些属性可以通过缓冲区对象(如上面的 “byteBuffer” ),获取实际值,如( byteBuffer.capacity();))
三、NIO直接缓冲区与非直接缓冲区
(一)基本概念
|-----------非直接缓冲区:通过“allocate”方法分配的缓冲区,是建立在 JVM内存中的。
如图:
(当用户程序需要读数据的时候,首先是从“物理磁盘”读数据到“内核地址空间”,然后“复制copy”一份到“用户地址空间”,用户程序再在从“用户地址空间中读取数据”,反之如图)
|-----------直接缓冲区:通过“allocateDirect”方法分配的缓冲区,是建立在 物理内存上的,在某种情况下是可以提高效率的。
如图:
(直接缓冲区是将原来的“copy”部分换成“物理内存映射文件” , 当用户程序写数据的时候,就将该数据写到这个 映射文件中,之后物理磁盘直接从这个物理磁盘获取数据,反之用户程序也可以直接从映射文件中读取数据。也就少了“copy”的开销)
所以“直接缓冲区”比“非直接缓冲区”效率要高
(二)直接缓冲区的缺点
当然,直接缓冲区也因为这种方式带来了一些缺点:
1、不安全
2、资源消耗比较大
3、当写入映射文件后,用户程序就不能够管理已经写入的数据了,其“分配”和“销毁”操作由操作系统决定(这里之前我以为是jvm虚拟机,,映射文件在直接内存,但是直接内存是用的本机内存(其实这里有点模糊,所以有其他见解一起交流哇))。
关于直接缓冲区与非直接缓冲区,官方API文档有如下简述:
四、通道Channel的原理与获取
早期的IO操作,当多个用户程序需要读写一个数据时,或者说多个IO操作,经过直接缓冲区或者非直接缓冲区,到达物理磁盘时,单个CPU操作这多个IO,然后再写入物理磁盘,最后写入内存。对于多并发IO来说,性能是非常不友好的。
如图:
于是就有了直接连接在内存与IO操作之间的“DMA总线” , 当有一个IO操作进来时,经过CPU的认证,建立一个直接连接内存和IO操作的总线,之后的IO读写就可以通过这个“DMA”总线直接读写数据到内存。
如图:
但是后来发现,这样仍然会对CPU性能不怎么友好,因为每一次IO操作都会判断。于是就有了“通道Channel” ,这个通道专门用于 IO操作, 就无需CPU判断。这种“直接的”“专门的”方式也就减轻了CPU很大的负担。
如图:
-------------------------------------Channel的基本操作-------------------------------------
1、概念:用于源节点与目标节点的连接,在java NIO中负责缓冲区中数据的传输,Channel本身不存储数据,因此需要配合缓冲区进行传输。
2、通道的主要实现类
java.nio.channels.Channel 接口:
|---------FileChannel
|---------SocketChannel
|---------ServerSocketChannel
|---------DatagramChannel
3、获取通道
|---------getChannel()方法
本地IO: FileInputStream / FileOutputStream 、RundomAccessFile
网络IO: Socket 、ServerSocket、DatagramSocket
|---------JDK 1.7 中的NIO .2针对各个通道提供了静态方法 open() ;
|---------JDK 1.7 中的NIO .2的Files 工具类的newByteChannel();
五、NIO 通道数据传输:直接缓冲与非直接缓冲的比较
代码demo:
/**
* @Author : WJ
* @Date : 2018/12/10/010 21:39
*
* 注释: 利用通道,基于“缓冲区”的方式进行文件复制
*/
public class Test {
/************************************非直接缓冲区方式************************************/
/**
*
* 效率较直接缓冲区方式“慢”,但相对“稳定”,对于IO操作不频繁以及IO连接时间不长的可以选用此方式。
*/
@org.junit.Test
public void test1() throws IOException {
//建立流
FileInputStream inputStream = new FileInputStream("1.jpg");
FileOutputStream outputStream = new FileOutputStream("2.jpg");
//获取通道
FileChannel inChannel = inputStream.getChannel();
FileChannel outChannel = outputStream.getChannel();
//分配指定大小的“非直接缓冲区”
ByteBuffer buf = ByteBuffer.allocate(1024);
//将通道中的数据存入缓冲区中
while (inChannel.read(buf)!= -1){
//切换到读取数据的模式
buf.flip();
//将缓冲区中的数据写入缓冲区中
outChannel.write(buf);
//清空缓冲区
buf.clear();
}
//最后关闭流和缓冲区
outChannel.close();
inChannel.close();
outputStream.close();
inputStream.close();
}
/*****************************************直接缓冲区方式************************************/
/**
*
* 通过“内存映射文件”方式复制数据
*
* 此种方式效率会很快,但是有些不稳定,有的时候,文件已经读写完成了,但是程序依旧在执行中:
* 这是因为当用户程序将数据写入内存映射文件中后,程序与数据就无关了,这个时候JVM的垃圾收集机制可能还没来得及回收用户程序
* 数据就已经读写完成了。
*
* 适用于: 长时间进行IO连接操作,大量IO操作等情况。
*
*/
public void test2()throws IOException{
/**
* 获取通道
* 这里有两个工具类:Paths 和 StandardOpenOption
*
* Paths 可以直接指定数据路径,也可多个字符串拼接路径
*
* StandardOpenOption.READ:允许读
* StandardOpenOption.WRITE:允许写
* StandardOpenOption.CREATE:允许创建(如果存在那么覆盖)
* StandardOpenOption.CREATE_NEW: 允许创建(如果存在则报错)
*/
FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"),StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//内存映射文件
MappedByteBuffer inMappedByteBuf = inChannel.map(FileChannel.MapMode.READ_ONLY
,0,inChannel.size());
MappedByteBuffer outMappedByteBuf = outChannel.map(FileChannel.MapMode.READ_WRITE
,0,inChannel.size());
//直接对缓冲区进行数据的读写操作
byte[] temp = new byte[inMappedByteBuf.limit()];
//读到内存映射文件中
inMappedByteBuf.get(temp);
//写到物理磁盘中去
outMappedByteBuf.put(temp);
}
/**
* 当然直接缓冲还有一种更为直接的使用方法
*
* 情况与上相同
*
*/
public void test3() throws IOException{
//首先获取通道的方式不变
FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"),StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//读写数据
inChannel.transferTo(0,inChannel.size(),outChannel);
//或者
outChannel.transferFrom(inChannel,0,inChannel.size());
/**上面两种没有什么区别:也就是从哪来,或者到哪去*/
//关闭通道
outChannel.close();
inChannel.close();
}
}
六、分散读取与聚集写入
代码demo:
/*********************************分散读取与聚集写入************************************/
/**
* 分散读取:一次从一个通道 读取多个 缓冲区
* 聚集写入;一个写入多个缓冲区 到一个通道
* @throws IOException
*/
public void test4() throws IOException{
RandomAccessFile randomAccessFile = new RandomAccessFile("文件路径","rw");
//获取通道
FileChannel channel = randomAccessFile.getChannel();
//分配指定大小的缓冲区若干个
ByteBuffer byteBuffer1 = ByteBuffer.allocate(100);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(200);
ByteBuffer byteBuffer3 = ByteBuffer.allocate(1024);
/***************分散读取*********************/
ByteBuffer[] buffers = {byteBuffer1,byteBuffer2,byteBuffer3};
channel.read(buffers);
/***************聚集写入*********************/
RandomAccessFile randomAccessFile1 = new RandomAccessFile("文件路径","rw");
FileChannel channel1 = randomAccessFile1.getChannel();
channel1.write(buffers);
}
七、NIO阻塞与非阻塞
(一)阻塞:
在传统IO单线程处理模式中,客户端发起一个读写请求,如果这个请求不是真实有效的,那么将会造成阻塞,后面的线程无法进来,也就造成了性能的急剧下降。
原来我们解决这个问题是采用 “多线程的IO”:
将用户请求分配到多个线程中,当一个线程被阻塞后,其他线程仍然可以继续请求。这也是多线程的优点。
但是,那些被阻塞的线程,那个线程后面的仍然无法请求,所以这种方式也不是最好的解决方案。
所以上面两种就是“传统IO”阻塞的缺陷。
(二)非阻塞
在客户端与服务端之间,有一个“Selector选择器” ,这个选择器时刻将所有 来自客户端的“通道(Channel)” 进行判断是否准备就绪,当通道准备就绪,选择器就 将 这个 通道,也就是客户端发过来的请求任务 分配到一个或者多个线程上,在此之前,服务端可以 自己完成自己的事情,这样也就 增加了CPU的利用。
以下于2019年5月3日更新
八、Buffer的相关操作
1、buffer的创建
//buffer的创建
ByteBuffer bu = ByteBuffer.allocate(1024);
//从既有数组中创建
byte array[] = new byte[1024];
ByteBuffer bu = ByteBuffer.wrap(array);
2、重置和清空缓冲区
①rewind()将position置零,并清除标志位
②clear()也将position置零,同时将limit设置为capacity大小,也就是回到最初的样子。
③flip()先将limit设置到positon所在位置,然后将positon置零,并清除标志位,这是应用于读写操作时的转换。
3、标志缓冲区
mark()方法,标记当前位置为标志位,当下次调用reset()方法时将会使position回到此位置。
4、复制缓冲区
duplicate():生成一个完全一样的缓冲区,读写互不干扰
5、缓冲区分片
slice():从现有的缓冲区中,创建新的子缓冲区,子缓冲区和父缓冲区共享数据。
6、只读缓冲区
asReadOnlyBuffer():得到一个与当前缓冲区一致的,并且共享内存数据的只读缓冲区。只读缓冲区只能被读,不能被写,当前缓冲区修改,会同步到只读缓冲区。
7、文件映射到内存
比基于流的方式要快得多,代码创建如下:
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,0,1024);
以上代码是将文件的前1024个字节映射到内存中,mao()方法返回一个MappedByteBuffer。