Java中文件操作功能小结
作者:HAibiiin 发布时间:2023-06-20 03:32:28
文件写入
为提供相对较高性能的文件读写操作,这里果断选择了 NIO 对文件的操作,因为业务背景需要数据的安全落盘。这里主要采用 ByteBuffer 与 FileChannel 的组合,下面是代码片段示例:
public static void write(String file, String content) throws IOException {
ByteBuffer writeBuffer = ByteBuffer.allocate(4096);
int cap = buffer.capacity();
try (FileChannel fileChannel = FileChannel.open(Path.of(file), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
byte[] tmp = content.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < tmp.length; i = i + cap) {
if (tmp.length < i + cap) {
buffer.put(tmp, i, tmp.length - i);
} else {
buffer.put(tmp, i, cap);
}
buffer.flip();
fileChannel.write(buffer);
buffer.compact();
}
fileChannel.force(false);
} finally {
buffer.clear();
}
}
ByteBuffer
在上面的代码(基于JDK11)片段中,我们使用 ByteBuffer 作为待读写数据的载体才能够配合 FileChannel 一起使用。如果是 JDK8 获取 FileChannel 可以采用 new RandomAccessFile(new File("xx"), "rw").getChannel()
。在讲 ByteBuffer 初始化之前,我们需要先对数据单位有一个明确的概念。
KB 不是 kb
我们常看到的 kb 单位对应 kilobits ,而 KB 单位对应 kilobyte。Java 中的 1 byte 对应 8 bits,所以 1 KB(1024 byte) = 8kb (8196 bits)。包括mb、MB等也是一样的,为方便记忆,我们只需要记住小写的 b 表示 bits,而大写的 B 表示 byte 即可。
接下来初始化采用 allocate()
方法,容量是 4096,因为 ByteBuffer 底层数据结构是 byte 数组,再结合上面的知识,我们这里创建了 4KB 大小的 Buffer。具体大小需要根据实际测试进行调整,普遍的说法是 4KB 的整数倍会发挥最大性能优势。
为什么是 4KB 的整数倍呢?大致就是, 操作系统一次 I/O 操作会以 I/O 块为单位进行操作,这个 I/O 块的默认大小是 4KB。但是这个数值并不严谨,它受操作系统,磁盘等因素影响,所以需要实际测试后调整。
初始化
另一种初始化的方式是通过 wrap()
对已存在 byte 数组进行包装,应用场景会略有不同,两者区别如下代码片段所示:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw createCapacityException(capacity);
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0)
}
public static ByteBuffer wrap(byte[] array, int offset, int length) {
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
HeapByteBuffer(byte[] buf, int off, int len) {
super(-1, off, off + len, buf.length, buf, 0)
}
最终调用的都是 Buffer(int mark, int pos, int lim, int cap)
这个初始化方法,该方法也揭示了 ByteBuffer 的基本属性:
position:表示下一个读写操作的起始位置,可通过
position()
方法获取;limit:表示下一个读写操作的最大位置,可通过
limit()
方法获取;capacity:表示容量,可通过
capacity()
方法获取;mark:自定义标记位置;
上述4个属性的关系始终满足:mark <= position <= limit <= capacity。在初始化后ByteBuffer的内部结构如下图所示:
ByteBuffer 操作及属性变化
通过上图中结构为 ByteBuffer 初始化的结构,写文件需要向 buffer 中写入数据,ByteBuffer 提供了多个 put()
方法,调用 put()
相关方法之后,如下图所示向 buffer 写入 8 个byte的内容后,其内部结构主要是 position 指向了后续插入数据的位置:
目前数据已经写入了 buffer 中,接下来需要通过 FileChannel 写入文件,年需要将数据从 buffer 中读出来。在调用 FileChannel 的 write()
方法之前,需要调用 buffer 的 flip()
方法,flip()
方法将标识属性变换为下图所示,也就是切换为读取模式,即 position 重置到 0,而 limit 移动到原 position 位置。这样从 position 读取到 limit 就是刚刚写入的数据:
FileChannel 完成 write 操作后,即 buffer 内数据读取完,则 position 的位置会移动到 limit 所在位置。为保证数据的完整性,此时需要调用 buffer 的 compact()
方法将 position 到 limit 间未读取的数据移动到 buffer 的头部,开启新的一轮写入模式,调用方法后具体的属性关系如下图所示(下图中例子为数据读 3 个 byte 后调用compact()
效果,将 position 与 limit 间的数据移动到 buffer 的头部,并将 limit 移动到 capacity 的位置,position 移动到未读数据的末尾):
最后在整个写文件的结尾,需要通过 FileChannel 的 force()
方法将数据强制刷盘,其实上面的所有操作只是将数据写入了 PageCache 中,具体何时落入磁盘由操作系统调度,而 force()
方法就是通知操作系统将 PageCache 的内容写入磁盘,这样才可以确保数据真正的持久化到磁盘中。
DirectByteBuffer
还有一种方式是通过 allocateDirect()
方法创建 DirectByteBuffer 采用对外内存,如果需要更高的性能,或者需要长期且大数据量的 I/O 操作可以采用这种方式。但一定要注意代码片段确保的 ((DirectBuffer) buffer).cleaner().clear()
对堆外内存进行回收(该方法在 JDK11 版本不可直接使用)。
如果不及时清理也会造成内存溢出。如下图所示,左侧为未调用 clear()
方法前的堆外内存使用情况,右侧为调用后的情况。同时可以配合JVM 参数 -XX:MaxDirectMemorySize 一起使用避免防止内存申请过大而导致进程被终止;
文件读取
这里我们将文件读取的代码片段摘录如下,关于文件读取主要是注意中文字符的乱码问题,因为我们定义的 buffer 是有容量的,一个容量读满之后,可能一个中文字符并没有读取完整。因为一个中文字符可能需要 2-3 个 byte,有可能存在只读取 1 个 byte 的情况。
所以需要结合 CharBuffer 对未读取完整的中文字符进行缓冲。具体代码示例如下所示:
public static String read(String file) throws IOException {
StringBuilder content = new StringBuilder();
ByteBuffer buffer = ByteBuffer.allocate(4096);
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
CharBuffer cb = CharBuffer.allocate(4096);
try (FileChannel fileChannel = FileChannel.open(Path.of(file), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
while (fileChannel.read(buffer) != -1) {
buffer.flip();
//从ByteBuffer读取数据到CharBuffer,最后如果不是完整的字符position的位置不会移动
//可以认为ByteBuffer中对应的字符未被读取
decoder.decode(buffer, cb, false);
cb.flip();
content.append(cb, cb.position(), cb.limit());
//将CharBuffer的position强制重制为0
cb.rewind();
buffer.compact();
}
} finally {
cb.clear();
buffer.clear();
}
return content.toString();
}
并发写入
FileChannel 的 read/write 操作均是线程安全的,但是因为我们不能保证数据被一次性写入,所以数据最终落在文件上会是混乱的片段。这里我们采用类似分区写的方式,每个线程负责写入一个分区文件,最后再执行合并操作。
同时这里介绍下 FileLock 这一进程级别的文件锁,它不能够对同一虚拟机内多个线程对文件的访问提供锁的能力。而且该锁的具体实现逻辑和操作系统有强相关。
来源:https://juejin.cn/post/7235833850277593125


猜你喜欢
- filter自定义过滤器 增加了 对验证码的校验package com.youxiong.filter;import com.y
- 介绍跨域CORS,全称是"跨域资源共享"(Cross-origin resource sharing)当页面发出跨域请求
- 1.打开File >> setting,选择Plugins>>Browse Repositories2.搜索Jreb
- 前言:现在的手机品牌和型号越来越多,导致我们平时写布局的时候会在个不同的移动设备上显示的效果不同,比如我们的设计稿一个View的大小是300
- 一.组合widget实现1.android和flutter自定义控件对比Android中,一般会继承View或已经存在的某个控件,然后覆盖d
- 一、下载Android Studio百度搜索“Android Studio"点击中文社区进入,选择最新版本下载。下载后双击安装包开
- SpringMvc中普通类注入Service为null场景:使用Quartz定时器时,普通的java类需要注入spring的service类
- 最新开发新项目的时候,要做分享项目,要求分享有微信,微信朋友圈,QQ,QQ空间,新浪微博这五个,所分享内容包括,分享纯图片,纯文字,图文类型
- 我最近在研究Spring框架的路上,那么今天也算个学习笔记吧!学习一下如何实现Bean的装配方法Bean的简介Java开发者一般会听过Jav
- 现在已经进入了2018年,Android 8.0系统也逐渐开始普及起来了。三星今年推出的最新旗舰机Galaxy S9已经搭载了Android
- 本文实例为大家分享了android实现录屏功能的具体代码,供大家参考,具体内容如下1、mian.activitypackage com.fp
- 一、分析 本次博客,主要解决文件上传等一系列问题,将从两方面来论述,
- 网上有不少教程,那个提示框字符集都是事先写好的,例如用一个String[] 数组去包含了这些数据,但是,我们也可以吧用户输入的作为历史记录保
- 本文实例为大家分享了Java模拟实现斗地主发牌的具体代码,供大家参考,具体内容如下题目:模拟斗地主的发牌实现,54张牌,每张牌不同的花色(红
- 一.内容抽象类当编写一个类时,常常会为该类定义一些方法,这些方法用于描述这个类的行为。但在某些情况下只需要定义出一些方法,而不需要具体的去实
- 进阶JavaSE-三大接口:Comparator、Comparable和Cloneable。Comparable和Comparator这两个
- 序言for循环语句是java循环语句中最常用的循环语句,一般用在循环次数已知的情况下使用。for循环语句的语法格式如下:java语言 for
- springboot集成开发实现商场秒杀加入主要依赖<dependency> <groupId>org.spring
- 这篇文章主要介绍了Java JDBC导致的反序列化攻击原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,
- 实例如下:一 json optString 解析的TimesTamp string二 long dateSec = (long) (Doub