如何用Java模拟外部程序占用文件导致File#delete()删除失败
问题背景
当前正在排查Logback相关问题:使用日志文件读取工具glogg打开日志文件时,Compressor类无法删除该文件;该问题仅在使用glogg时出现,用记事本打开文件则无异常,推测和文件锁定机制有关。
目前需要用Java演示程序复现该场景,无需手动启动glogg。
需求说明
需要实现以下效果:
- 启动线程A和线程B
- 线程A负责向文件X写入内容
- 当线程A调用
File#delete()方法删除X时返回false(即删除失败) - 删除失败的原因是线程B占用了该文件,阻止了删除操作成功执行
已尝试的实现代码
public void run() { while (true) { logDumper.debug(bigString); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] s) { final Runnable runnable = () -> { try { var fileInputStream = new FileInputStream("./test.log"); var file = new File("./test.log"); fileInputStream.readAllBytes(); System.out.println("Size: " + file.length()); final var fc = fileInputStream.getChannel(); // this entire locking thing does not seem to have any effect final var shared = true; // required to avoid throwing when opening a file as read-only try (FileLock lock = fc.tryLock(0,file.length(), shared)) { } catch (NonReadableChannelException| NonWritableChannelException e) { logger.error("Something happened", e); fc.close(); } } catch (IOException e) { logger.error("Crashed on locking", e); e.printStackTrace(); } }; new Thread(runnable).start(); new Main().run(); }
原代码失效原因
你遇到的glogg可复现、记事本无法复现的核心逻辑是:glogg打开日志文件后会一直持有文件句柄用于后续增量日志监听,而记事本是把文件内容全部读入内存后就立刻关闭了文件句柄,所以不会影响删除操作。
你写的代码没有复现成功,主要有两个问题:
- 你用了try-with-resources语法管理FileLock,代码块执行完成后会自动释放锁、关闭文件通道和文件流,相当于线程B读完文件就立刻释放了所有占用,自然不会影响后续删除
- 不同系统的文件删除逻辑有差异:Windows下只要有线程持有文件的打开句柄,就会禁止删除;类Unix下文件删除默认是解除链接,只要有进程持有文件描述符,文件数据不会被释放,但
delete()方法会返回成功,默认场景很难复现删除失败的效果
可运行的实现方案
Windows环境
Windows下不需要额外加锁,只要线程B一直持有文件输入流不关闭,就能阻止文件删除:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; public class FileOccupyDemo { private static final Logger logger = LoggerFactory.getLogger(FileOccupyDemo.class); private static final String TEST_FILE = "./test.log"; // 控制线程B是否释放文件流的开关 private static volatile boolean keepHold = true; public static void main(String[] args) throws InterruptedException { // 初始化测试文件,模拟线程A写入的操作 initTestFile(); // 启动线程B,模拟glogg的行为,一直持有文件句柄 Thread threadB = new Thread(() -> { try (FileInputStream fis = new FileInputStream(TEST_FILE)) { System.out.println("线程B已打开文件,保持持有中"); // 循环等待,不关闭流 while (keepHold) { Thread.sleep(100); } } catch (IOException | InterruptedException e) { logger.error("线程B执行异常", e); } }); threadB.start(); // 等待1秒保证线程B已经完成文件打开操作 Thread.sleep(1000); // 线程A尝试删除文件 Thread threadA = new Thread(() -> { File file = new File(TEST_FILE); boolean deleteResult = file.delete(); System.out.printf("线程A删除文件结果:%b%n", deleteResult); if (!deleteResult) { System.out.println("删除失败,原因:文件被线程B持有打开的句柄"); } // 通知线程B可以释放资源 keepHold = false; }); threadA.start(); } private static void initTestFile() { try (FileWriter fw = new FileWriter(TEST_FILE)) { fw.write("测试日志内容,模拟Logback写入的日志文件"); } catch (IOException e) { logger.error("初始化测试文件失败", e); } } }
类Unix环境(Linux/macOS)
类Unix系统默认无法通过持有句柄让File#delete()返回false,需要额外开启强制锁支持:
- 给对应分区挂载时添加
mand参数开启强制锁支持 - 修改测试文件权限,关闭组执行权限:
chmod g-x test.log - 线程B获取排他锁且一直持有不释放,此时线程A调用delete就会返回false
内容的提问来源于stack exchange,提问作者oligofren
相关产品推荐
相关产品推荐

