Java/Kotlin中File实例是否线程安全?多线程/协程并发写文件丢行问题
核心结论
java.io.File 实例只是文件系统路径的轻量包装,不代表对底层物理文件的独占访问权。哪怕每个线程/协程都持有独立的局部File实例,只要这些实例指向磁盘上同一路径的文件,本质上都是在操作同一个文件,并发写入必然会出现内容覆盖、行丢失的问题。
代码中存在的两个具体并发缺陷
- 文件存在性检查与文件创建、写入操作非原子,存在竞态
多个协程同时首次触发写入时,可能同时通过!file.exists()判断,全部进入新建文件分支。后执行createNewFile()的协程会直接覆盖之前协程创建并写入了内容的文件,导致已写入的表头、记录直接丢失。 - 多写入端同时打开文件追加写入无同步保障
即使所有协程都进入追加写入分支,BufferedWriter自带内存缓冲区,多个写入端的缓冲区刷新顺序不可控,叠加操作系统层面普通文件写入无原子性保障,会出现内容交错、部分缓冲区内容被覆盖的问题,直接表现为部分行丢失、内容错位。
问题复现代码
private val noOfThreads = newFixedThreadPoolContext(nThreads = 3, name = "coRoutineThreads") val nameOfFile = "test.csv" fun coRoutineFunction(){ runBlocking{ for(i in 1..100) { async(noOfThreads){ writeToFile(nameOfFile) } } } } fun writeToFile(nameOfFile){ val file = File(fileName) // 基于bufferedWriter和CSVPrinter执行写入 }
存在缺陷的写入实现
internal fun writeToFile(fileName: String, recordList: List<Map<String, String>>) { try { val file = File(fileName) if (!file.exists()) { file.createNewFile() val writer = FileWriter(file) val bufferedWriter = BufferedWriter(writer) writeCSVRecordWithHeader(bufferedWriter, recordList) bufferedWriter.close() } else { val writer = FileWriter(file, true) val bufferedWriter = BufferedWriter(writer) concCSVRecord(bufferedWriter, recordList) bufferedWriter.close() } } catch (exception: Exception) { logger.error("Exception at writeRecordsToFile while creating file: ${ExceptionUtils.getStackTrace(exception)}") throw exception } }
修复方案
- 对同个文件的全流程写入操作加互斥同步
协程场景下可以用Mutex为每个文件路径绑定独立的锁实例,将「检查文件是否存在、创建文件/打开追加流、写入内容、关闭流」的完整逻辑包裹在锁范围内,保证同一时间只有一个协程执行写入操作,从根本上消除竞态。 - 采用单协程统一写入的架构避免多端并发写
单独启动一个专属写入协程,所有待写入的CSV记录通过Channel发送到这个协程,由它统一负责文件的打开、初始化、写入全流程。这种方案没有锁的性能开销,也从架构上杜绝了多写入端同时操作同一个文件的可能,是协程场景下的最优实现。 - 剥离文件初始化逻辑
把「创建文件、写入CSV表头」的逻辑从每次写入的分支判断中移除,只在程序启动后第一次写入前执行一次,消除存在性检查带来的竞态点。
内容的提问来源于stack exchange,提问作者msdev
相关产品推荐
相关产品推荐

