部分引用:
【Java NIO】一文了解NIO - puyangsky - 博客园 (cnblogs.com)
原版IO是BIO,阻塞io,而jdk1.4后的是NIO,non-blocking IO,非阻塞io。应用《Java NIO》中的一段话来解释一下NIO出现的原因:
操作系统与java基于流的IO模型不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存储DMA的协助下完成,而JVM的原生IO喜欢操作小数据库(面向流,单个字节,几行文本等)。结果就是操作系统送来整个缓冲区的数据,java.io的流数据再花了大量时间把它们拆成了小数据块,往往拷贝一个小数据块就要往返几层对象。操作系统喜欢整卡车似的去运来数据,而Java.io类喜欢一铲子一铲子去加工数据。有了NIO后,就可以轻松的把一卡车的数据备份到可以直接使用的ByteBuffer对象。Java的RandomAccesssFile类是比较接近操作系统的方式。
因此java原生的IO模型之所以慢,是因为与操作系统的操作方式不匹配造成,那么NIO比BIO快最主要就是用到了缓冲区的技术了。
总结就是IO是面向流的,NIO是面向缓冲区的。IO面向流是指每次只能从流中读取一个或者多个字节,直到读完,也就是对应操作小数据块,被读取到的内容没有被缓存起来。而NIO能直接提前缓存大数据块,再进行进程内部的缓冲区的流读取,效率更快。
如图描述的是操作系统是如何把磁盘空间与进程缓冲区建立通道的:
不那么恰当的理解:内核缓冲区可以理解为cpu缓存,磁盘就是硬盘这样。
而因为用户是无法直接操作硬件的,因此需要通过系统调用让操作系统的内核去操作磁盘。另外磁盘这种块存储设备操作的是固定大小的数据块,而用户请求的则是非规则大小的数据,内核空间在这里的作用就是分解、重组的作用。
最主要的三个依赖组件为:缓冲区Buffer,通道Channel和选择器Selector
nio流的相关类都放在java.nio包中,大体如下:
Buffer是一个抽象类,其子类实现类有如下,如名字所说,具体的Buffer就是以具体的为单位的缓冲区。
**其中ByteBuffer使用最多,即以字节为单位的缓冲区。**需要用的时候查文档即可。目前掌握ByteBuffer就够了
缓冲区对象会有一系列属性:【即创建缓冲区对象后可以访问和操作的属性】
属性都在Buffer总抽象类中。
另外从源码我们可以看到ByteBuffer中的方法基本都是静态方法,隐藏了很多ByteBuffer的实例实现类,方便了用户的调用。所以我们在得到byteBuffer的对象后,其属性和方法并不是在ByteBuffer类中,而是在其实现类如HeapByteBuffer类中。
方法一:allocate静态方法,直接分配的方式创建:
// 最常用的创建,创建全新缓冲区
// [其中allocate是分配的意思,其单位默认是类中的byte]
int capacity = 1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
allocate静态方法源码:
方法二:wrap静态方法,把已存在的字节数组包装到缓冲区:
// 以字节数组为容器创建缓冲区,也就是说这个字节数组就是我们的缓冲区
// 对缓冲区操作 = 对数组操作;对数组操作 = 对缓冲区操作
// [其中wrap是囊、容器的意思,也就是以某字节数组为缓冲区容器]
byte[] array = new byte[1024];
ByteBuffer byteBuffer = ByteBuffer.wrap(array);
// 当然可以限定容器范围,提供offet
3.1.2.1 flip翻转
这是什么东西呢?缓冲区ByteBuffer会不断被二进制填充,填充满后就是进程写好或者从内核读好的一批缓冲数据,也就是进行下一步,把缓冲区内容传递到channel通道,这个时候如果我们直接读取Buffer缓冲区,其实是读取不到东西的,因为这个时候的position指针指向的是Buffer末尾,因为已经满了嘛。如果我们要再放入通道之前再次读取Buffer,就需要把指针返回到头部,也就是需要缓冲区翻转。
方法在Buffer抽象类中,是final方法,它对应的实例可以使用。
int capacity = 1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
xxxxx
byteBuffer.flip();
xxxxx
flip翻转的源码:用于认为当前缓冲区已满,这个满不是说到capacity,而是主观认为满了
limit:缓冲区当前大小即当前position位置,position在数组末尾。即认为当前缓冲区已满
position:当前位置要回到头
mark:标记点重置为-1
3.1.2.2 rewind翻转
与flip效果一致,区别在于使用时机,flip用于确定当前buffer已经满了;而如果rewind是用于当前缓冲区还没满的时候使用。
int capacity = 1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
xxxxx
byteBuffer.flip();
xxxxx
rewind翻转的源码:用于当前缓冲区还没满
区别是limit没有被操作,也就是认为当前缓冲区未满
3.1.2.3 clear清空
清空缓冲区内容
int capacity = 1024
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
clear清空的源码:并不是实际意义上的删除,而是覆盖
实际是把position指针归0,下次写入的时候会覆盖之前的内容。
3.1.2.4 remaining()
remaining = limit - position
表示我还剩余多长, 一般用在flip之后,表示我剩余多少没有读,因此flip后position = 0;
3.1.2.5 mark() 与 reset()
// mark = position
byteBuffer.mark()
// positon = mark
byteBuffer.reset()
3.1.2.6 limit() 限制缓冲区使用范围
int newLimit = xxx; // limit要小于capacity
// limit = newLimit;
// if(position > limit) position = newLimit; 如果position超过了limit,限制其位置
byteBuffer.limit(newLimit);
3.1.2.7 操作index的其他方法
nextGetIndex()
nextPutIndex()
checkIndex(int i)
等等,不常用就不介绍了
Buffer总结:
最常用的:
进程中Buffer缓冲区为我们装载了数据,但是数据的写入和读取并不能直接进行与内核缓冲区的read()与write()的系统调用,JVM为我们提供了一层对系统调用的封装,Channel可以用最小的开销来访问操作系统本身的IO服务,解耦同时优化开销。
Channel分类:
【区别于java.net.socket,socket套接字是用来写服务器通信的,原生的net.socket是阻塞的,而nio的socket是可以实现非阻塞的,可以做比如聊天软件等服务。但java不是我们常用写服务器的语言。做服务器C++更加适合。因此我们只介绍我们要用的FileChannel】
3.2.1.1 获取FileChannel
FileChannel只能通过工厂方法来实例化,即调用RandomAccessFile、FileInputStream和FileOutputStream的getChannel()方法。
【RandomAccessFile支持随机访问文件,程序可以直接跳转到文件的任意地方来读写数据,而另外两个只能从头到尾找到对应位置后读写】
因此RandomAccessFile是读写文件的比较优的解,最重要的场景就是网络请求的多线程下载与断点续传
String mode = "rw"
RandomAccessFile file = new RandomAccessFile("文件路径", mode);
/*其中mode有如下选择:
"r": 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
"rw": 打开以便读取和写入。
"rws": 打开以便读取和写入。相对于 "rw","rws" 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。
"rwd": 打开以便读取和写入,相对于 "rw","rwd" 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。
*/
3.2.1.2 使用FileChannel
FileChannel是能读也能写的双工通道
// FileChannel源码:
// 把通道中数据传到目的缓冲区中,dst是destination的缩写
public abstract int read(ByteBuffer dst) throws IOException;
// 把源缓冲区中的内容写到指定的通道中去
public abstract int write(ByteBuffer src) throws IOException;
读 实际使用流程:
public static void readFile(String path) throws IOException {
FileChannel fc = new RandomAccessFile("文件路径", "r").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// StringBuilder是用来接收文件文本结果
StringBuilder sb = new StringBuilder();
// 每次循环读一波,如果channel连接的文件流没有内容读了,就返回 -1
// 每次读完,limit会自动到有内容的最后一个位置,而不是每次都 = capacity
while ((fc.read(buffer)) >= 0) {
// 每次读完,buffer是满的,翻转指针,保证从头开始读
buffer.flip();
// remaining = limit - position 表示剩下多长
// 一定要注意,这个时候position经过了flip后是等于0的
// 所以一般这个时候buffer.remaining() = capacity - 0; 就是buffer的整长
// 也表示剩下多少没读
// 这样写主要是为了最后一波的读,不一定是满的,最后一次读就是limit - 0 就是有内容的,不去读无内容的
byte[] bytes = new byte[buffer.remaining()];
// 从buffer中获取内容放到bytes数组中
buffer.get(bytes);
// 依据bytes数组转换当前数据
String string = new String(bytes, "UTF-8");
// 把当前数据加入到总结果中
sb.append(string);
// 清空buffer,给下一次内容读取的空间
buffer.clear();
}
System.out.println(sb.toString());
}
写 实际使用流程
public static void writeFile(String path, String string) throws IOException {
FileChannel fc = new RandomAccessFile("文件路径", "rw").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 指针,表示写入进度
int current = 0;
// 要写的内容的字节数组的长度,为目标字节数组
int len = string.getBytes().length;
while (current < len) {
// 每次写入1024字节到ByteBuffer中
// 如果剩余内容不足1024,则提前break
for (int i=0;i<1024;i++) {
if (current+i>=len) break;
buffer.put(string.getBytes()[current+i]);
}
//指针一次性跳转1024。如果是最后一次的缓冲,则跳转小于1024
current += buffer.position();
// buffer翻转,从头开始写
buffer.flip();
// 通过channel通道写入
fc.write(buffer);
// 清空buffer数组提供下一次缓冲的空间
buffer.clear();
}
}
总结:
FileChannel固然用法简单,但是要注意,FileChannel是阻塞的!!!!并且无法切换到非阻塞状态,这和NIO的non-blocking理念有所冲突。但是能够满足大部分小文件和少文件的场景使用。
如果要实现NIO的非阻塞模式,需要使用套接字通道+选择器。
经过了解,从socket开始往后的内容是属于使用nio编写服务器进行socket通信了,而原来的java.net.socket是阻塞的。目前这并不是我们在对文件操作中需要用到的。因此不再展示后面的内容了。而且代码复杂度太高了。
选择器其实是一种多路复用机制在Java语言中的应用,在学习Selector之前有必要学习一下I/O多路复用的概念。【多路复用是非阻塞同步的】
多路复用:
linux的select模型,poll模型和epoll模型就是多路复用的经典。一个socket通道会监听多个资源,只要某个资源准备好了read或者write了,那当前通道就提供给对应需要该资源的请求。而不需要一个请求开一个通道。poll模型是通过轮询去监听,而epoll是通过资源响应来实现高效监听。如果资源没准备好,那么请求所在的线程就先做其他事情,没必要挂起阻塞。
IO多路复用就是通过某种机制可以监视多个文件描述符,一旦某个文件可以执行IO操作,能够通知应用程序去进行相应的读写操作。
因此多路复用也可以理解为一个线程去监听多个网络连接,线程定时轮询所有的网络连接,某个准备好了,该线程就会给这个连接提供服务。而对于还没有准备好网络连接所在的请求,先不进行IO服务,先去做其他事情【因此是不阻塞的,不是说做不了IO就挂起】,等该请求对应的网络连接准备好了,再通知应用程序去使用该线程提供的服务进行IO。
因此要使用Selector,必须要使用非阻塞的Channel。【Socket通道】
Selector是要在使用java编写服务器进行非阻塞通信的时候才会使用,我们暂时不需要使用。
nio包中还有Files类和Paths类也是我们在操作文件的时候经常使用的
一般就是通过get()方法返回一个Path类型的,代表当前资源的路径,提供给其他一些的nio相关类使用。
Path path = Path.get("xxx/xxx/xx.jpg");
!!下面每个使用都附带方法对应的源码!!
4.2.0 获取文件大小
long size = Files.size(Path.get("/xxxx/xxx.jpg"));//得到的结果是B,Byte字节
//提供一个转换单位的方法
String fileSize = "";
double len = Double.valueOf(file.length());
if(len < 1024) {
fileSize = "" + String.format("%.2f", len) + "b";
}else if(len >= 1024 && len < 1048576) {
fileSize = "" + String.format("%.2f", len/1024) + "kb";
}else if(len >= 1048576 && len < 1073741824) {
fileSize = "" + String.format("%.2f", len/1048576) + "mb";
}
4.2.1 建立一个对文件某资源的input流
InputStream inputStream = Files.newInputStream(Path.get("/xxxx/xxx.jpg"));
4.2.2 建立一个对某文件资源的output流
OutputStream outputStream = Files.newOutputStream(Path.get("/xxxx/xxx.jpg"));
4.2.3 建立一个对某文件夹的流
DirectoryStream directoryStream = Files.newDirectory(Path.get("/xxxx"));
4.2.4 创建一个文件
Files.createFile(Path.get("/xxxx/xxx.jpg"));
4.2.5 创建一个文件夹
Files.createFile(Path.get("/xxxx"));
4.2.6 删除文件/文件夹
Files.delete(Path.get("/xxxx/xxx.jpg"));
4.2.7 复制文件
Files.copy(Path.get("/xxxx/xxx1.jpg"), Path.get("/xxxx/xxx2.jpg"));
4.2.8 移动文件
Files.move(Path.get("/xxxx/xxx1.jpg"), Path.get("/xxxx/xxx2.jpg"));
4.2.9 判断是否为文件夹
Files.isDirectory(Path.get("/xxxx/xxx"));
4.2.10 还有包括下面常用的
//判断两个文件是否相同
public static boolean isSameFile(Path path, Path path2)
//判断该文件是否被隐藏
public static boolean isHidden(Path path)
//获取当前文件最后被修改的时间
public static FileTime getLastModifiedTime(Path path, LinkOption... options)
//设置当前文件最后被修改的时间
public static Path setLastModifiedTime(Path path, FileTime time)
//当前文件是否存在
public static boolean exists(Path path, LinkOption... options)
//当前文件是否可读
public static boolean isReadable(Path path)
//当前文件是否可写
public static boolean isWritable(Path path)
//当前文件是否可执行
public static boolean isExecutable(Path path)
//建立一个缓冲流
public static BufferedReader newBufferedReader(Path path)
public static BufferedWriter newBufferedWriter(Path path)
//读一个文件,需要我们提供一个绑定文件的流,initialSize是缓冲字节数组的初始化大小
private static byte[] read(InputStream source, int initialSize)
//读所有的行,返回的是一个数组列表,可以用来处理数据
public static List<String> readAllLines(Path path)
//把字节数组写入一个文件
public static Path write(Path path, byte[] bytes)
//获取某目录下的所有文件和目录(Path)
public static Stream<Path> list(Path dir)
//求文件的行数
public static Stream<String> lines(Path path)
**总结一句话:**NIO相比于原生IO,如果仅用于对文件的操作【只使用FileChannel】而不使用socket通信,那么它还是阻塞的,但是因为在IO线程中引入了Buffer空间与Channel的封装,使得我们在读写文件的时候效率更高【原生IO需要操作5000ms,NIO可能就只需要500ms】。
**最后整理NIO文本读写的工具类。**直接复制使用即可。
public class NioUtil {
/**
* NIO读取文件
* @throws IOException
*/
public static String read(String url) throws IOException {
RandomAccessFile access = new RandomAccessFile(url, "r");
FileChannel channel = access.getChannel();
int allocate = 1024;
ByteBuffer byteBuffer = ByteBuffer.allocate(allocate);
// 接收结果的容器
StringBuilder sb = new StringBuilder();
while ((channel.read(byteBuffer)) >= 0) {
// 每次读完,buffer是满的,翻转指针,保证从头开始读
byteBuffer.flip();
// remaining = limit - position 表示剩下多长
// 一定要注意,这个时候position经过了flip后是等于0的
// 所以一般这个时候buffer.remaining() = capacity - 0; 就是buffer的整长
// 也表示剩下多少没读
// 这样写主要是为了最后一波的读,不一定是满的,最后一次读就是limit - 0 就是有内容的,不去读无内容的
byte[] bytes = new byte[byteBuffer.remaining()];
// 从buffer中获取内容放到bytes数组中
byteBuffer.get(bytes);
// 依据bytes数组转换当前数据
String string = new String(bytes, "UTF-8");
// 把当前数据加入到总结果中
sb.append(string);
// 清空buffer,给下一次内容读取的空间
byteBuffer.clear();
}
channel.close();
if (access != null) {
access.close();
}
return sb.toString();
}
/**
* NIO写文件, 默认覆盖
* @param text 要写入的文本
* @param url 绝对路径
* @throws IOException
*/
public static void write(String url, String text) throws IOException{
RandomAccessFile access = new RandomAccessFile(url, "w");
FileChannel fc = access.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 指针,表示写入进度
int cur = 0;
// 要写的内容的字节数组的长度,为目标字节数组
int len = text.getBytes().length;
while (cur < len) {
// 每次写入1024字节到ByteBuffer中
// 如果剩余内容不足1024,则提前break
for (int i = 0; i < 1024; i++) {
if (cur + i >= len) break;
buffer.put(text.getBytes()[cur+i]);
}
//指针一次性跳转1024。如果是最后一次的缓冲,则跳转小于1024
cur += buffer.position();
// buffer翻转,从头开始写
buffer.flip();
// 通过channel通道写入
fc.write(buffer);
// 清空buffer数组提供下一次缓冲的空间
buffer.clear();
}
}
/**
* NIO写文件,可追加
* @param text 要写入的文本
* @param url 绝对路径
* @throws IOException
*/
public static void write(String url, String text, String mode) throws IOException{
RandomAccessFile access = new RandomAccessFile(url, "rw");
FileChannel fc = access.getChannel();
if("a".equals(mode)) {
//把文件指针指向末尾进行添加
access.seek(access.length());
}
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 指针,表示写入进度
int cur = 0;
// 要写的内容的字节数组的长度,为目标字节数组
int len = text.getBytes().length;
while (cur < len) {
// 每次写入1024字节到ByteBuffer中
// 如果剩余内容不足1024,则提前break
for (int i = 0; i < 1024; i++) {
if (cur + i >= len) break;
buffer.put(text.getBytes()[cur+i]);
}
//指针一次性跳转1024。如果是最后一次的缓冲,则跳转小于1024
cur += buffer.position();
// buffer翻转,从头开始写
buffer.flip();
// 通过channel通道写入
fc.write(buffer);
// 清空buffer数组提供下一次缓冲的空间
buffer.clear();
}
}
public static void main(String[] args) throws Exception {
String url ="C:\\xxx.text";
write(url, "123", "a");
System.out.println(read(url));
}
}