同时创建新文件

5
要创建一个新的、唯一的文件名,我使用以下代码:
File file = new File(name);
synchronized (sync) {
    int cnt = 0;
    while (file.exists()) {
        file = new File(name + " (" + (cnt++) + ")");
    }
    file.createNewFile();
}

接下来,我使用文件并将其删除。 当我在多线程情况下执行此操作时,有时会在file.createNewFile()上出现异常:

java.io.IOException: Access is denied
    at java.io.WinNTFileSystem.createFileExclusively(Native Method)
    at java.io.File.createNewFile(File.java:1012)

以下代码可以重现问题(大部分情况下):
final int runs = 1000;
final int threads = 5;
final String name = "c:\\temp\\files\\file";
final byte[] bytes = getSomeBytes();
final Object sync = new Object();

ExecutorService exec = Executors.newFixedThreadPool(threads);
for (int thread = 0; thread < threads; thread++) {
    final String id = "Runnable " + thread;
    exec.execute(new Runnable() {
        public void run() {
            for (int i = 0; i < runs; i++) {
                try {
                    File file = new File(name);
                    synchronized (sync) {
                        int cnt = 0;
                        while (file.exists()) {
                            file = new File(name + " (" + (cnt++) + ")");
                        }
                        file.createNewFile();
                    }

                    Files.write(file.toPath(), bytes);
                    file.delete();
                } catch (Exception ex) {
                    System.err.println(id + ": exception after " + i
                            + " runs: " + ex.getMessage());
                    ex.printStackTrace();
                    return;
                }
            }
            System.out.println(id + " finished fine");
        }
    });
}
exec.shutdown();
while (!exec.awaitTermination(1, TimeUnit.SECONDS));

方法getSomeBytes()仅生成指定数量的字节,其实际内容并不重要:
byte[] getSomeBytes() throws UnsupportedEncodingException,
        IOException {
    byte[] alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYZ1234567890\r\n"
            .getBytes("UTF-8");
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
        for (int i = 0; i < 100000; i++) {
            baos.write(alphabet);
        }
        baos.flush();
        return baos.toByteArray();
    }
}

当我执行这段代码时,有时会顺利运行,但大多数情况下,它会生成一些异常,例如下面的输出结果:
Runnable 1: exception after 235 runs: Access is denied
java.io.IOException: Access is denied
    at java.io.WinNTFileSystem.createFileExclusively(Native Method)
    at java.io.File.createNewFile(File.java:1012)
    at test.CreateFilesTest$1.run(CreateFilesTest.java:36)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Runnable 4: exception after 316 runs: Access is denied
java.io.IOException: Access is denied
    at java.io.WinNTFileSystem.createFileExclusively(Native Method)
    at java.io.File.createNewFile(File.java:1012)
    at test.CreateFilesTest$1.run(CreateFilesTest.java:36)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Runnable 2: exception after 327 runs: Access is denied
java.io.IOException: Access is denied
    at java.io.WinNTFileSystem.createFileExclusively(Native Method)
    at java.io.File.createNewFile(File.java:1012)
    at test.CreateFilesTest$1.run(CreateFilesTest.java:36)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Runnable 3 finished fine
Runnable 0 finished fine

有什么想法吗?我已在安装了Java 1.7.0_45和1.8.0_31的Windows 8计算机上进行了测试,结果相同。
不确定问题是否与此问题相同,但可能是。在同一进程中使用多个线程似乎是问题的一部分,但我无法确定,它可以更快地复现。

2
File.createTempFile() 可能是一个更加干净的方法,无论文件是否真的被打算为临时文件。 - Sneftel
@Sneftel:我同意,但是文件的名称确实很重要,所以我不能在这里使用File.createTempFile。 - Steven
2个回答

5

在Windows平台上,如果刚刚删除了同名文件,即使是单线程应用程序,createNewFile也可能会随机失败。有关详细信息,请参见此问题。要解决您的问题,您可以尝试忽略createNewFile中的IOException并继续执行。类似于以下内容:

synchronized (sync) {
    int cnt = 0;
    while (true) {
        try {
            if(file.createNewFile())
                break;
        } catch (IOException e) {
            // continue;
        }
        file = new File(name + " (" + (cnt++) + ")");
    }
}

请注意,您不需要检查file.exists()的调用,因为createNewFile()方便地返回了它是否成功创建了文件。
请注意,如果您控制所有创建的临时文件并且不关心确切的文件名,则通常无需锁定。您可以使用全局AtomicLong获取下一个文件名或将线程ID附加到文件名中。

由于问题可能是由外部因素引起的,我猜我要在这种情况下尝试使用try/catch来解决问题。 - Steven

0

你的循环不是安全的。存在时间窗口问题。应该更像这样:

while (!file.createNewFile()) {
        file = new File(name + " (" + (cnt++) + ")");
    }

1
你能否详细解释一下在这个同步块中,“timing-window”是什么意思?另外,你修改后的代码和原始代码一样也无法正常运行。 - Tagir Valeev
@TagirValeev 在exists()createNewFile()之间存在一个时间窗口,此时另一个线程可以创建该文件。还未检查createNewFile()的结果。如果我在这里发布的两行循环失败,则必须存在平台问题,该方法无法按照广告中所述工作。实际上,此循环与您答案中的代码没有任何区别,除了catch块。你如何解释这个? - user207421
1
请注意,所有线程都在同一个监视器上同步,因此另一个线程无法执行此操作。 - Tagir Valeev
@TagirValeev 所以另一个进程可以创建它。仍然存在时间窗口问题。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接