如何在FileWriter上设置BufferedWriter的缓冲区大小

9

当我使用多线程向单个文件写入数据时,我在BufferedWriter遇到了问题。

我设置了BufferedWriter的缓冲区大小,但无论我设置什么数字,它都会在缓冲区为8192(默认缓冲区大小)时将数据刷新到磁盘上,而不是我设置的大小(这里是16384)。我的代码有问题吗?

以下是我构建BufferedWriter的方式:

new BufferedWriter(new FileWriter(fileName, true), 16384);

这是完整的代码:
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class Test1 {
    public static void main(String[] args) throws IOException {
        for(int i =0;i<10;i++){
            MyThread r = new MyThread();
            Thread t = new Thread(r);
            t.start();
        }
    }
}

class MyThread implements Runnable {
    public void run() {
        String s = "{addffffffkkkljlkj2015dd}\n";
        BufferedWriter bw = null;
        try {
            bw = new BufferedWriter(new FileWriter(
                    "/Users/liaoliuqing/Downloads/1.txt", true),16384);
        } catch (IOException e) {
            e.printStackTrace();
        }
        for(int i =0 ; i<1000; i++){
            try {
                bw.write(String.format("%03d", i)+s);
                //bw.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

我在Debian和Mac上运行程序,得到了相同的结果。无论我如何改变缓冲区大小,都没有任何效果。 - jinhong_lu
1
你想要什么效果? - Lærne
如果缓冲区大小设置为16384,则应在数据达到16384字节时写入磁盘,而不是8192。现在无论我设置什么,它都会在每8192字节时写入磁盘。 - jinhong_lu
FileWriter 内部缓冲区:https://dev59.com/0lfUa4cB1Zd3GeqPIoK7 文档中说“要自己指定[字符编码和字节缓冲区大小],请在 FileOutputStream 上构造 OutputStreamWriter”,但我没有看到这些类的任何构造函数允许您指定缓冲区大小 :( - Chris Martin
4个回答

4

我的代码有问题吗?

有一些问题。主要是:可能存在IO和并发错误。文件缓冲区大小可能不太重要(而且你无法有效地处理它)。

  • 尝试打开已经打开的文件。所有线程都试图写入同一个文件(1.txt),这可能会导致问题。FileWriter文档说:

    某些平台允许仅由一个FileWriter(或其他文件写入对象)打开文件进行写入。在这种情况下,如果所涉及的文件已经打开,则此类构造函数将失败。

  • 行可能会被切断和混合。如果您有几个线程,它们各自的缓冲区在某个时候刷新到相同的输出,您甚至可能不需要奇怪的竞态条件或在线程中间停止或写操作来查看您的输出是否损坏。

    作为解决方案(如果您的线程必须共享相同的输出),您可以使用具有同步访问的共享对象来处理实际写入。我在我的示例中实现了SafeAppender,但可能有更好的替代方案。

  • 没有刷新关闭缓冲区将意味着(尾部)数据将丢失(就像雨中的泪水一样)。通常使用finally块来处理这个问题。

  • 另外,正如其他用户所述,BufferedWriter 缓冲区大小不影响FileOutputStream(因此也不影响FileWriter)中的缓冲区大小。看起来java.iojava.nio API没有任何方法可以干扰它。如果您查看Java库源代码,您可能会注意到BufferedWriter缓冲区大小只意味着在实际写入委托输出之前存储的字符数。默认大小(8192)对于大多数情况都是最佳的,增加它可能意味着带来更多麻烦(潜在地丢失更多数据)而不是好处。

这是我的代码,如果对你有用:

// https://dev59.com/5Y7ea4cB1Zd3GeqPG-H8
public class TestWriter {

public static class SafeAppender {
    private BufferedWriter bw;
    private int users = 0;
    public SafeAppender(File f) throws IOException {
        bw = new BufferedWriter(new FileWriter(f));
    }

    public synchronized void append(String s) throws IOException {
        bw.write(s);
    }
    public synchronized void incrUsers() { 
        users ++; 
    }
    public synchronized void decrUsers() {
        if (--users <= 0) {
            try {
                bw.flush();
                System.err.println("INFO-appender-flush()");
            } catch (Throwable whatever) { /* log-if-you-care*/}
        }
    }
    // Might be called by GC, or not
    @Override protected void finalize() throws Throwable {
        try {
            bw.close();
            System.err.println("INFO-appender-close()");
        } catch (Throwable whatever) { /* log-if-you-care */}
        super.finalize();
    }
}

private static class MyRunnable implements Runnable {
    final static String S = "{addffffffkkkljlkj2015dd}";
    SafeAppender appender;
    String threadId;
    public MyRunnable (SafeAppender a, String tid) {
        appender = a; threadId = tid;
    }

    public void run() {
        appender.incrUsers();
        try {
            for(int i =0 ; i<1000; i++){
                // NOTE: Not a good idea to printStackTrace if each line fails. Let thread fail
                String line = String.format("%s-%03d-%s\n", threadId, i, S);
                appender.append(line);
            }
        } catch (IOException e) {
            System.err.printf("ERROR-%s-%s\n", threadId, e.toString());
        } finally {
            appender.decrUsers();
        }
    }
}

public static void main(String[] args) {
    try {
        File f = File.createTempFile("TestWriter", ".txt");
        System.err.printf("INFO-main-Writing into %s\n", f.getCanonicalPath());
        SafeAppender appender = new SafeAppender (f);
        for(int i =0;i<10;i++){
            MyRunnable r = new MyRunnable(appender, ""+i);
            Thread t = new Thread(r);
            t.start();
        }
    } catch (Throwable e) {
        e.printStackTrace(System.err);
    }
}

}

3

FileWriter 实际上使用自己的固定大小为1024字节的缓冲区。另一方面,BufferedWriter 使用8192字节(默认值)的缓冲区大小,并且可以由用户配置为任何其他所需大小。

进一步混淆问题的是,Java 6 实现的 OutputStreamWriter 实际上委托给一个 StreamEncoder,它使用自己的缓冲区,默认大小为8192字节。而且,StreamEncoder 缓冲区是可由用户配置的,尽管无法通过封装的 OutputStreamWriter 直接访问它。


2
这个答案似乎是从 https://dev59.com/eVrUa4cB1Zd3GeqPfwbd#6976933 复制而来的,其中包含一些额外有用的链接。 - Chris Martin
确实是这样。但“复制”可能不是一个正确的词。我本可以重新表述来暗示我的答案,但重点是传递信息,如果已经有了,那么重写它没有意义。 我跳过了那些链接,以保持答案简洁和相关,并为OP打开了一些研究的大门。 :) - Raman Shrivastava

2

我使用OutputStream而不是Writer解决了这个问题,以下是代码:

bw = new BufferedOutputStream(
                new FileOutputStream(new File("/Users/liaoliuqing/Downloads/1.txt"),true),165537);

虽然这对你来说很好,但很奇怪,因为“Writer”委托了“OutputStream”,所有的行为(附加标志、缓冲区大小、写入ASCII时的确切操作)都是相同的。还有其他变化吗? - Javier

0
你所看到的不是缓冲 BufferedWriter 的大小,而是 FileWriter 内部使用的缓冲区的大小。引用 Java 文档(http://docs.oracle.com/javase/7/docs/api/java/io/FileWriter.html):
“此类的构造函数假定默认字符编码和默认字节缓冲区大小是可接受的。要自己指定这些值,请在 FileOutputStream 上构造 OutputStreamWriter。”
因此,如果您想对数据实际写入磁盘的时间进行精细控制,应将 BufferedWriter 实例化为:
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File('my_file.txt),true)));

那怎么帮助你控制缓冲区大小呢? - Chris Martin
我这样设置的:bw = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(new File("/Users/liaoliuqing/Downloads/1.txt"),true)),65536); bw.write(String.format("%03d", i)+s); bw.flush(); 我的字符串大小为27340字节,所以缓冲区大小足够包含整个字符串。但是它仍然每8192字节就写入磁盘。 - jinhong_lu
我的答案是基于文档的:正如它所述“指定这些值”(复数),我假设在这种情况下FileOutputStream不会有自己的缓冲区.... - Marco Sandrini

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