Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
51940 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

14 | 文件IO:实现高效正确的文件读写并非易事

你好,我是朱晔。今天,我们来聊聊如何实现高效、正确的文件操作。
随着数据库系统的成熟和普及,需要直接做文件 IO 操作的需求越来越少,这就导致我们对相关 API 不够熟悉,以至于遇到类似文件导出、三方文件对账等需求时,只能临时抱佛脚,随意搜索一些代码完成需求,出现性能问题或者 Bug 后不知从何处入手。
今天这篇文章,我就会从字符编码、缓冲区和文件句柄释放这 3 个常见问题出发,和你分享如何解决与文件操作相关的性能问题或者 Bug。如果你对文件操作相关的 API 不够熟悉,可以查看Oracle 官网的介绍

文件读写需要确保字符编码一致

有一个项目需要读取三方的对账文件定时对账,原先一直是单机处理的,没什么问题。后来为了提升性能,使用双节点同时处理对账,每一个节点处理部分对账数据,但新增的节点在处理文件中中文的时候总是读取到乱码。
程序代码都是一致的,为什么老节点就不会有问题呢?我们知道,这很可能是写代码时没有注意编码问题导致的。接下来,我们就分析下这个问题吧。
为模拟这个场景,我们使用 GBK 编码把“你好 hi”写入一个名为 hello.txt 的文本文件,然后直接以字节数组形式读取文件内容,转换为十六进制字符串输出到日志中:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(23)

  • 最新
  • 精选
  • Darren
    今天算是打开了一片新的天地,因为日常的开发设计文件的不太多,竟然不知道有Files这样的牛逼操作,之前对于JDK相关的NIO关注的也不多,真的是打开了一闪窗。 先说下FileChannel 的 transfreTo 方法,这个方法出现在眼前很多次,因为之前看Kafka为什么吞吐量达的原因的时候,提到了2点:批处理思想和零拷贝; 批处理思想:就是对于Kafka内部很多地方来说,不是消息来了就发送,而是有攒一波发送一次,这样对于吞吐量有极大的提升,对于需要实时处理的情况,Kafka就不是很适合的原因; 零拷贝:Kafka快的另外一个原因是零拷贝,避免了内存态到内核态以及网络的拷贝,直接读取文件,发送到网络出去,零拷贝的含义不是没有拷贝,而是没有用户态到核心态的拷贝。 而在提到零拷贝的实现时,Java中说的就是FileChannel 的 transfreTo 方法。 然后回答下问题: 第一个问题: Files的相关方法文档描述: When not using the try-with-resources construct, then directory stream's close method should be invoked after iteration is completed so as to free any resources held for the open directory. 所以是需要手动关闭的。 第二个问题: 没有原子操作,因此是线程不安全的。个人理解,其实即使加上了原子操作,也是鸡肋,不实用的很,原因是:File 类和 Files的相关操作,其实都是调用操作系统的文件系统操作,这个文件除了JVM操作外,可能别的也在操作,因此还不如不保证,完全基于操作系统的文件系统去保证相关操作的正确性。

    作者回复: 👍🏻👍🏻👍🏻👍🏻

    10
    78
  • 👽
    这篇专栏内容不算多,但是感觉可扩展的地方很多。 我认为:读写字符流乱码,其实本质上在于环境不一致的问题。其实跟日志路径之类的问题思路一致。服务器保存日志,如果配置绝对路径,C盘下的log文件夹。等部署到服务器上就会出错。除了针对不同的环境使用不同的配置,还可以尝试使用相对路径,亦或者将路径以存数据库的方式持久化。其实本质上,跟编码格式的处理方式一样,尽量屏蔽不同环境之间的差异。 保证文件流释放,同样也可以延伸。不仅仅是文件流,任何涉及资源占用问题的时候,都需要考虑资源是否可以保证被释放。try-whit-resources来解决IO流,其实同样也应用于各种需要释放资源的场景。 关于缓冲区,个人理解,一个典型的应用就是数据库的分页查询。如果将所有数据一次查出,不但消耗资源,甚至有可能内存不够。如果一次只查一个,如果需要查询的是几十条数据,频繁进行数据库访问,性能也较差。所以,采取了折中的方案,分页查询,一次仅查出一部分数据。既不会内存溢出,也保证了响应速度。

    作者回复: 不错

    16
  • 梦倚栏杆
    第一个问题:都间接实现了autoCloseable接口,所以都可以使用try-with-resources进行释放。 第二个非原子性,没有锁,也没有异常后的回滚。需要调用方进行事务控制

    作者回复: 不错

    13
  • Geek_3b1096
    谢谢老师详细解说文件操作

    作者回复: 不客气,觉得好可以点赞转发

    5
  • 汝林外史
    BufferedInputStream的二级缓冲什么时候能用到呢?既然需要自己定义一个缓冲,比如2K,那么肯定也是控制一次读取2K,应该不会有读取超过2K的时候吧?

    作者回复: 你可能会疑惑了,既然这样使用 BufferedInputStream 和 BufferedOutputStream 有什么意义呢?其实,这里我是为了演示所以示例三使用了固定大小的缓冲区,但在实际代码中每次需要读取的字节数很可能不是固定的,有的时候读取几个字节,有的时候读取几百字节,这个时候有一个固定大小较大的缓冲,也就是使用 BufferedInputStream 和 BufferedOutputStream 做为后备的稳定的二次缓冲,就非常有意义了

    9
    5
  • Michael
    特别喜欢老师的这种工匠精神,对读者的每一个问题都精心回复

    作者回复: 感谢认可

    3
  • Monday
    gbk与utf8区别(本文总结所得): 1、gbk只适合中文编码方式,utf8全世界的编码方式 2、对于中文汉字,gbk使用2个字节,utf8使用3个字节;对于英文字母都是1个字节。这种变长编码方式,怎么区分汉字和英文呢?

    作者回复: 针对你的问题可以看一下http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html 其中『UTF-8 的编码规则很简单,只有二条』一节就明白了

    3
    3
  • pedro
    第二个问题,不是原子的,所以需要注意,如果 io 异常,可能会出现复制后的文件不完整,文件未删除成功等问题

    作者回复: 是,其实我在思考题中也会补充更多正文无法详细阐述的坑

    3
  • 珅珅君
    你好,关于BufferedInputStream有一点疑问,如果我构造BufferedInputStream的时候设置缓冲流的大小是1kb,这里缓冲区叫A,但是调用read(byte[])的时候额外用的是8kb缓冲,这里缓冲区叫B,那么读取文件的时候,这两种缓冲的大小的工作流程是什么样的。先B后A还是?

    作者回复: 先A后B(可能多次)

    2
  • Demon.Lee
    LongAdder longAdder = new LongAdder(); IntStream.rangeClosed(1, 1000000).forEach(i -> { try { try (Stream<String> lines = Files.lines(Paths.get("demo.txt"))) { lines.forEach(line -> longAdder.increment()); } } catch (IOException e) { e.printStackTrace(); } }); log.info("total : {}", longAdder.longValue()); ------------------------------------------------------ 我一开始还奇怪为啥要两个try,catch放里面不就行了么,想了一下才明白,是为了捕获里面释放资源的异常,相当于捕获finally中的异常。 两道题都去翻了源码,第一题我觉得也是需要主动释放,Path也算一种fd吧,不太确定;第二题没有看到锁什么的,不是原子性的,不过创建或删除文件,重复处理,操作系统层会报错,但写内容到文件中就需要注意了。

    作者回复: 直接放到catch里是可以的,这里我因为修改了wrong所以写成这样了。对于释放资源产生的异常,同样可以在catch中捕获,可以看一下try-with-resources语法糖会翻译成怎么样的代码就理解了。

    1
收起评论
显示
设置
留言
23
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部