写入文件的最快方法是什么?

41

我创建了一个方法,接受一个File和一个String。 它用给定的字符串替换文件内容并生成一个新文件。

这就是我写的代码:

public static void Save(File file, String textToSave) {

    file.delete();
    try {
        BufferedWriter out = new BufferedWriter(new FileWriter(file));
        out.write(textToSave);
        out.close();
    } catch (IOException e) {
    }
}

然而它非常慢,有时需要超过一分钟的时间。

我该如何写入数以万计甚至百万个字符的大型文件?


9
删掉文件是不必要的,你正在覆盖它。 - Carles Barrobés
3
与您的问题不直接相关:您可以考虑重新构造 out.close() 语句,以便它可以在 finally 块中完成。如果在写入时出现错误,它仍然会关闭。 - Rocky Madden
2
不要忽略你的IOException,否则可能导致程序以神秘的方式失败。 - Peter Lawrey
5
与其在写入文件之前删除或直接覆盖它,我建议先写入一个临时文件,然后再将其重命名为旧文件。这样,如果 IO 在中途失败,您就不会冒着用损坏的东西替换旧文件的风险。 - Tom Anderson
1
这个问题明显是错误的,怎么会得到21个赞?OP甚至承认它是错误的 - 实际的I/O并没有导致长时间等待。 - user949300
显示剩余3条评论
7个回答

27

请确保分配的缓冲区足够大:

BufferedWriter out = new BufferedWriter(new FileWriter(file), 32768);

你正在运行什么样的操作系统?这也可能会产生很大的差异。但是,花费一分钟去编写一个小于巨大文件大小的文件听起来像是系统问题。在Linux或其他*ix系统上,您可以使用类似于strace的工具来查看JVM是否正在进行大量不必要的系统调用。(很久以前,Java I/O相当愚蠢,如果不小心,会进行疯狂的大量低级write()系统调用,但是我所说的“很久以前”指的是1998年左右。)

编辑—请注意,Java程序以简单方式编写简单文件,但速度确实很慢是一种本质上奇怪的情况。您能否观察到在写入文件时CPU的负载很高吗?不应该有这种情况;这样的事情几乎不应该对CPU产生任何负载。


同意。由于他将字符串作为参数进行处理,他甚至可以提前知道所需的缓冲区大小:textToSave.getBytes().length。 - Rocky Madden
@Rocky Madden,是的,这是一个非常好的观点。然而,通过Java IO库转储字符串应该以任何方式都非常快。 - Pointy
调整缓冲区大小时,getBytes() 可能非常昂贵。我建议您将其设置为256K并不再担心它。 - Peter Lawrey
1
-1 是因为如果你正在编写一个单独的大字符串,你甚至不需要字符缓冲区 - 你可以直接将它传递给 FileWriter,它会在一个批处理中处理它。也许在字节级别上有一个缓冲区是值得的(使用 OutputStreamWriter + BufferedOutputStream + FileOutputStream),因为字符编码是用一个大小你无法控制的缓冲区来完成的,而且我认为这个缓冲区相当小。但不是在字符级别上。 - Tom Anderson
2
好的答案。事实证明,它写得很慢的原因实际上不是因为写入方法,而是因为我使用了如此长的“字符串”。计算“字符串”所花费的时间太长了,而写入并没有花费那么多时间。我的解决方案是分段写入文件,而不是一次性全部写入,这样要写入的“字符串”就不会变得非常庞大。你提供的想法也有所帮助。 - user263078
显示剩余3条评论

25

一个简单的测试给你

char[] chars = new char[100*1024*1024];
Arrays.fill(chars, 'A');
String text = new String(chars);
long start = System.nanoTime();
BufferedWriter bw = new BufferedWriter(new FileWriter("/tmp/a.txt"));
bw.write(text);
bw.close();
long time = System.nanoTime() - start;
System.out.println("Wrote " + chars.length*1000L/time+" MB/s.");

打印

Wrote 135 MB/s.

5

3

尝试使用内存映射文件:

FileChannel rwChannel = new RandomAccessFile("textfile.txt", "rw").getChannel();
ByteBuffer wrBuf = rwChannel.map(FileChannel.MapMode.READ_WRITE, 0, textToSave.length());

wrBuf.put(textToSave.getBytes());

rwChannel.close();

2

你好,我已经创建了两种方法来创建大文件,在运行Windows 7、64位、8GB RAM机器、JDK 8的程序下,以下是结果。
在这两种情况下,都创建了一个包含从1到2000万(印度系统中的2亿)每行数字的180MB文件。

Java程序的内存逐渐增长到600MB

第一个输出

Approach = approach-1 (Using FileWriter)
Completed file writing in milli seconds = 4521 milli seconds.

第二个输出

Approach = approach-2 (Using FileChannel and ByteBuffer)
Completed file writing in milli seconds = 3590 milli seconds.

一个观察 - 在第二种方法中,我正在计算位置(pos变量),如果我将其注释掉,则仅最后一个字符串可见,因为在位置上被覆盖,但时间减少到近2000毫秒。

附加代码。

import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.TimeUnit;

public class TestLargeFile {

    public static void main(String[] args) {
        writeBigFile();
    }

    private static void writeBigFile() {
        System.out.println("--------writeBigFile-----------");
        long nanoTime = System.nanoTime();
        String fn = "big-file.txt";
        boolean approach1 = false;
        System.out.println("Approach = " + (approach1 ? "approach-1" : "approach-2"));
        int numLines = 20_000_000;
        try {
            if (approach1) {
                //Approach 1 -- for 2 crore lines takes 4.5 seconds with 180 mb file size
                approach1(fn, numLines);
            } else {
                //Approach 2 -- for 2 crore lines takes nearly 2 to 2.5 seconds with 180 mb file size
                approach2(fn, numLines);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("Completed file writing in milli seconds = " + TimeUnit.MILLISECONDS.convert((System.nanoTime() - nanoTime), TimeUnit.NANOSECONDS));
    }

    private static void approach2(String fn, int numLines) throws IOException {
        StringBuilder sb = new StringBuilder();
        FileChannel rwChannel = new RandomAccessFile(fn, "rw").getChannel();
        ByteBuffer wrBuf;

        int pos = 0;
        for (int i = 1; i <= numLines; i++) {
            sb.append(i).append(System.lineSeparator());
            if (i % 100000 == 0) {
                wrBuf = rwChannel.map(FileChannel.MapMode.READ_WRITE, pos, sb.length());
                pos += sb.length();
                wrBuf.put(sb.toString().getBytes());
                sb = new StringBuilder();
            }
        }
        if (sb.length() > 0) {
            wrBuf = rwChannel.map(FileChannel.MapMode.READ_WRITE, pos, sb.length());
            wrBuf.put(sb.toString().getBytes());
        }
        rwChannel.close();
    }

    private static void approach1(String fn, int numLines) throws IOException {
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= numLines; i++) {
            sb.append(i).append(System.lineSeparator());
        }
        FileWriter fileWriter = new FileWriter(fn);
        fileWriter.write(sb.toString());
        fileWriter.flush();
        fileWriter.close();
    }
}

0

这个解决方案使用Java NIO创建一个包含字符串“ABCD...89\n”的20GB文件,重复10 * 2亿次。在MacBook Pro(2021年的14英寸,M1 Pro,SSD AP1024R)上的写入性能约为5.1 GB/s。

代码如下:

public static void main(String[] args) throws IOException {
    long number_of_lines = 1024 * 1024 * 200;
    int repeats = 10;
    byte[] buffer = "ABCD...89\n".getBytes();
    FileChannel rwChannel = FileChannel.open(Path.of("textfile.txt"), StandardOpenOption.CREATE, StandardOpenOption.WRITE);

    // prepare buffer
    ByteBuffer wrBuf = ByteBuffer.allocate(buffer.length * (int) number_of_lines);
    for (int i = 0; i < number_of_lines; i++)
        wrBuf.put(buffer);

    long t1 = System.currentTimeMillis();

    for(int i = 0; i < repeats; i++)  {
        rwChannel.write(wrBuf);
        wrBuf.flip();
    }

    while (wrBuf.hasRemaining()) {
        rwChannel.write(wrBuf);
    }

    long t2 = System.currentTimeMillis();

    System.out.println("Time: " + (t2-t1));
    System.out.println("Speed: " + ((double) number_of_lines * buffer.length*10 / (1024*1024)) / ((t2-t1) / (double) 1000) + " Mb/s");
}

-3
在Java中,BufferWriter非常慢:直接使用本地方法,并尽可能少地调用它们(每次调用尽量提供尽可能多的数据)。
    try{
        FileOutputStream file=new FileOutputStream(file);
        file.write(content);
        file.close();
    }catch(Throwable e){
        D.error(e);
    }//try

此外,删除文件可能需要一些时间(可能首先被复制到回收站)。就像上面的代码一样,覆盖文件即可。


1
我个人在写服务器端 Java 代码很长时间以来,一直没有遇到 BufferedWriter 非常“慢”的经历。也许如果我有一个非常严肃的高吞吐量的应用程序,我可能不会选择使用它,但它并不那么糟糕;怎么可能呢? - Pointy
2
同样地,我从未见过对File#delete()的调用将文件移动到回收站。删除意味着永久删除。 - Kevin Day
Pointy:是的,也许我很久以前就通过MS调试器跟踪了Java文件写入操作,看到它在我的计算机上执行了无意义的系统调用。 - Kyle Lahnakoski

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