使用Java walkFileTree和SimpleFileVisitor删文件遇FileSystemException问题排查
问题
使用Java的walkFileTree结合SimpleFileVisitor删除文件时,同一目录下8个文件中有2个无法删除,报错提示文件被其他进程占用。相关代码及错误日志如下:
代码示例
public class ExtraFileCleaner implements AutoCloseable { private static final ExecutorService EXE_SERVICE = Executors.newFixedThreadPool(1); protected ExtraFileCleaner() { //TODO } public void deleteFilePath(final Path path) { if (path != null && Files.exists(path) && Files.isWritable(path)) { EXE_SERVICE.submit(() -> { try { Files.walkFileTree(path, new DeletingFileVistor()); } catch (NoSuchFileException e) { log.debug("Path does not exist: " + path); } catch (IOException e) { log.warn("Delete failed: {} ", e.getMessage()); } }); } } @Override public void close() throws InterruptedException { DELETE_SERVICE.shutdown(); if (!EXE_SERVICE.awaitTermination(1 * 60 * 1000, TimeUnit.MILLISECONDS)) { EXE_SERVICE.shutdownNow(); } } public static class DeletingFileVistor extends SimpleFileVisitor<Path> { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { log.info("visitFile: {}", path.toString()); return visit(path); } @Override public FileVisitResult postVisitDirectory(Path path, IOException exc) throws IOException { log.info("POSTvisitFile: {}", path.toString()); return visit(path); } private FileVisitResult visit(Path path) { FibonacciDelay delay = new FibonacciDelay(); for (int attempt = 0; attempt < delay.getMaxAttempts(); attempt++) { try { Files.deleteIfExists(path); log.info("Deleted: {}", path.toString()); break; } catch (IOException e) { String msg = e.getMessage(); log.info("exception-2 {}", msg); if (e instanceof FileSystemException && msg != null && msg.contains("The process cannot access the file because it is being used by another process")) { log.debug("File is in use: {}", path.toString()); } else { log.debug("{}: {}", new Object[]{e.getClass().getSimpleName(), e.getMessage()}); } delay.doWait(attempt); } if (attempt == delay.getMaxAttempts() - 1) { log.warn("Delete aborted: {}", path.toString()); } } return FileVisitResult.CONTINUE; } } }
错误日志
java.nio.file.FileSystemException: C:\ProgramData\XYZ\FileManager\local\work\2b5f439e-cb80-4be9-a4ff-c7c796ba\test.1.0.jar: The process cannot access the file because it is being used by another process. at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:92) at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103) at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108) at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274) at java.base/sun.nio.fs.AbstractFileSystemProvider.deleteIfExists(AbstractFileSystemProvider.java:110) at java.base/java.nio.file.Files.deleteIfExists(Files.java:1181) at com.zyz.abc.file.WorkDirectoryCleaner$DeletingFileVistor.visit(WorkDirectoryCleaner.java:110) at com.zyz.abc.file.WorkDirectoryCleaner$DeletingFileVistor.visitFile(WorkDirectoryCleaner.java:98) at com.zyz.abc.file.WorkDirectoryCleaner$DeletingFileVistor.visitFile(WorkDirectoryCleaner.java:94) at java.base/java.nio.file.Files.walkFileTree(Files.java:2725) at java.base/java.nio.file.Files.walkFileTree(Files.java:2797) at com.zyz.abc.file.WorkDirectoryCleaner.lambda$deletePath$0(WorkDirectoryCleaner.java:40) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829)
分析与解决方案
代码存在以下几处遗漏和优化点:
1. 遍历过程中文件句柄未及时释放
Files.walkFileTree遍历文件时会打开文件句柄读取属性,Windows系统下未释放的句柄会阻止文件删除。需在删除前尝试关闭可能残留的句柄:
@Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { log.info("visitFile: {}", path.toString()); // 尝试关闭可能占用的文件句柄 try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { channel.close(); } catch (IOException ignored) { // 忽略关闭失败,继续执行删除逻辑 } return visit(path); }
2. 线程池关闭逻辑的变量错误
close()方法中使用了未定义的DELETE_SERVICE,需改为EXE_SERVICE,否则线程池无法正常关闭,可能残留线程占用资源:
@Override public void close() throws InterruptedException { EXE_SERVICE.shutdown(); // 修正变量名 if (!EXE_SERVICE.awaitTermination(1 * 60 * 1000, TimeUnit.MILLISECONDS)) { EXE_SERVICE.shutdownNow(); } }
3. 重试逻辑的针对性不足
当前重试逻辑未针对“文件被占用”场景做特殊处理:
- 可延长斐波那契延迟的总等待时间,给其他进程足够的释放时间
- 增加日志级别,明确标记被占用文件的重试状态
4. 目录删除的依赖检查缺失
postVisitDirectory中直接删除目录,若目录内有未删除成功的文件,删除会失败。需先检查目录是否为空:
@Override public FileVisitResult postVisitDirectory(Path path, IOException exc) throws IOException { log.info("POSTvisitFile: {}", path.toString()); // 检查目录为空后再删除 if (Files.list(path).findAny().isEmpty()) { return visit(path); } else { log.warn("Directory not empty, skip delete: {}", path); return FileVisitResult.CONTINUE; } }
5. 权限检查时机不合理
deleteFilePath提前检查Files.isWritable(path),但提交到线程池执行时权限可能已变化,需将权限检查移至实际删除逻辑中,避免出现“检查时可写、删除时不可写”的矛盾情况。
内容的提问来源于stack exchange,提问作者JDGuide
相关产品推荐
相关产品推荐

