Java中Files.copy是否为线程安全函数?多线程并行拷贝同文件会出错吗
问题解答
1. 最初版本代码的线程安全性分析
首先明确结论:最初版本的代码不是线程安全的,理论上存在故障风险,主要问题有两个:
竞态条件问题
!dest.exists()判断和Files.copy操作之间存在时间差,多个线程同时执行时,可能出现以下时序:
- 线程A判断目标文件不存在,进入拷贝逻辑
- 在线程A完成拷贝前,线程B也判断目标文件不存在,同样进入拷贝逻辑
此时两个线程同时向同一个目标路径写入内容,会导致最终文件内容混杂、残缺,后续读取CSV时会出现格式错误。
半成文件被读取的问题
只要目标文件被创建(哪怕还没写完),dest.exists()就会返回true,后续线程会直接使用这个还在写入过程中的不完整文件,同样会引发业务错误。
你之前测试没有复现问题,只是因为测试场景没有触发极小概率的时间窗口,不代表理论上不存在故障可能。
2. 调整后加锁版本的问题
你修改后的加锁代码依旧没有解决线程安全问题,核心错误是:ReentrantLock是在方法内部创建的局部变量,每个线程调用该方法时都会生成一个独立的锁实例,锁完全没有互斥效果,等同于没有加锁,原来的竞态问题依旧存在。
3. 正确的线程安全实现方案
有两种常用的低成本方案可选:
方案1:使用原子文件操作(推荐,不需要额外加锁)
利用操作系统层面的原子操作避免竞态,逻辑如下:
private void prepareGroupDirectory() { new File(outputGroupFolderPath).mkdirs(); String map = instance.getUploadedMapPath().toString(); File source = new File(map); String myFile = FilenameUtils.getBaseName(map) + "." + FilenameUtils.getExtension(map); File dest = new File(outputGroupFolderPath + File.separator + "results_" + myFile); instance.setWritableMapForGroup(dest.getAbsolutePath()); try { // 先拷贝到同目录的临时文件,避免写入过程中被其他线程读取 File tempDest = new File(dest.getAbsolutePath() + "." + UUID.randomUUID() + ".tmp"); Files.copy(source.toPath(), tempDest.toPath(), StandardCopyOption.COPY_ATTRIBUTES); // 原子重命名到目标路径,操作系统保证要么成功要么完全失败,不会出现半成文件 Files.move(tempDest.toPath(), dest.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } catch (FileAlreadyExistsException e) { // 其他线程已经完成拷贝,直接忽略即可 } catch (Exception e) { // 其他异常按需处理 } }
这个方案的优势是不需要维护全局锁,依赖操作系统原子性保证,性能更高,也不会出现半成文件问题。
方案2:使用全局共享锁
如果不想改文件操作逻辑,可以把锁改成类的静态成员变量,让所有线程共用同一把锁:
// 类静态成员变量,所有实例共用同一把锁 private static final ReentrantLock dirLock = new ReentrantLock(); private void prepareGroupDirectory() { new File(outputGroupFolderPath).mkdirs(); String map = instance.getUploadedMapPath().toString(); File source = new File(map); String myFile = FilenameUtils.getBaseName(map) + "." + FilenameUtils.getExtension(map); File dest = new File(outputGroupFolderPath + File.separator + "results_" + myFile); instance.setWritableMapForGroup(dest.getAbsolutePath()); dirLock.lock(); try { if (!dest.exists()) { String pathToWritableMap = createCopyOfMap(source, dest); logger.info(pathToWritableMap); } } catch (Exception e) { // 按需处理异常 } finally { dirLock.unlock(); } }
该方案逻辑简单,缺点是锁粒度较大,多个不同目标文件的拷贝操作也会被互斥,性能略低。
内容的提问来源于stack exchange,提问作者mcool
相关产品推荐
相关产品推荐

