多线程环境下文件检查代码正确性及file.exists()特性咨询
多线程环境下文件存在性检查与双重检查锁实现的正确性问题
问题描述
需求是在多线程环境下检查服务器本地文件是否存在:若存在则直接返回内容;若不存在则从S3下载后写入本地再返回。给出的实现代码如下:
final Object lock = new Object(); File file = new File("/file/path"); if (file.exists()) { return FileUtils.readFileToByteArray(file); } else { byte[] bytes = this.downloadFileFromRemoteServer(); if (!file.exists()) { synchronized (lock) { if(!file.exists()) { FileUtils.writeByteArrayToFile(tempFile, bytes); } } } tempFile.renameTo(file); return bytes; }
代码试图模仿双重检查锁机制,现在有两个疑问:
file.exists()方法是否具备volatile关键字的特性?- 这段代码的实现是否正确?
回答
1. file.exists()是否具备volatile特性?
答案是不具备。
volatile关键字的核心作用是保证JVM内存中变量的可见性(一个线程对变量的修改能立即被其他线程感知)和禁止指令重排序。但file.exists()本质是调用操作系统本地方法查询外部文件系统的状态,和JVM内存变量完全是两回事:
- 不同线程调用
exists()时,可能读取的是操作系统的文件系统缓存,而非实时查询磁盘,导致一个线程创建文件后,另一个线程短时间内仍会返回false。 - JVM不会为
exists()调用插入内存屏障,所以它没有volatile语义的可见性保证,多线程下无法依赖它实时感知文件系统的变化。
2. 这段代码的实现存在多处严重问题,是不正确的
我们逐一拆解核心问题:
- 锁对象完全无效:代码中
lock是方法内的局部变量,每个线程调用该方法时都会创建全新的Object实例。这意味着synchronized(lock)起不到任何同步作用——每个线程都持有自己的锁,多个线程可以同时进入同步块,双重检查锁的逻辑直接失效。 - 重复下载浪费资源:第一个
if (file.exists())没有任何同步保护,多个线程可能同时判定文件不存在,进而同时调用downloadFileFromRemoteServer(),导致重复下载S3文件,白白消耗带宽和服务器资源。 - 临时文件与重命名的逻辑漏洞:
- 代码中
tempFile未定义,假设是预先创建的临时文件,那么多个线程下载完成后,即使同步块保证只有一个线程写入临时文件,后续所有线程都会执行tempFile.renameTo(file)。而renameTo()在目标文件(file)已存在时会返回false,后续线程的重命名操作会失败,但代码未处理这个返回值,可能引发逻辑异常。 - 写入临时文件和重命名的过程不是原子操作,若写入过程中出现异常,会留下不完整的临时文件,影响后续逻辑执行。
- 代码中
- 双重检查的可见性问题:下载完成后到进入同步块前的
if (!file.exists())检查,依然没有同步保护,且exists()没有volatile特性,无法保证此时读取的文件状态是最新的,可能导致无效的同步块执行。
简单修复思路
如果要实现正确的多线程逻辑,可以参考以下方向:
- 使用共享锁对象:将
lock定义为类的成员变量(而非方法局部变量),确保所有线程共用同一个锁。 - 调整逻辑顺序:先尝试获取锁再检查文件是否存在,从根源避免重复下载。
- 用原子性文件操作替代
renameTo():比如使用JDK的Files.move()方法,并处理文件操作的异常。 - 考虑使用成熟的缓存框架:比如Guava的
LoadingCache,它已经封装好了多线程下的缓存加载逻辑,能避免手动实现的各种坑。
内容的提问来源于stack exchange,提问作者Ryanqy




