背景
今天用多线程使用不同的BufferWriter往同一文件写内容,发生数据错位。
就是一行数据不完整,被分割了。如
abc
efg
变成了
abefg
c
然后我想起了之前写过一个demo,也是这样写的,但没发生数据错位问题呀!怎么这次就这么邪乎?
之前的demo是这样的:两个线程同时往test.txt文件写入1000行
"I am a java coder, i am strong!\n"
public class SingleThreadWriteFileDemo {
private static final Logger logger = LoggerFactory.getLogger(SingleThreadWriteFileDemo.class);
private static final String content = "I am a java coder, i am strong!\n";
public static void main(String[] args) throws InterruptedException {
twoThreadFileWriter();
}
private static void twoThreadFileWriter() {
Stream.of(1,2).forEach(i -> {
new Thread(SingleThreadWriteFileDemo::fileWriter).start();
});
}
public static void fileWriter() {
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("test.txt", true)))) {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
try {
writer.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
printDuration(start);
} catch (IOException e) {
e.printStackTrace();
}
}
}
最终的文件是正常的2000行 I am a java coder, i am strong!
我不太相信,又把之前的demo跑了好几遍,结果还是正常的2000行 I am a java coder, i am strong!
这时我就纳闷了,这java,是不是写点赞它的话,它就能给你跑正常?
于是乎我变本加厉,决定给它再加点彩虹屁,在内容加了个程度副词 very
"I am a java coder, i am very strong!\n"
这时候错位的情况再次出现了(果然人还是不能夸太多,你看他这不就飘了)
但我依旧很纳闷,为什么改了一下内容,数据就发生错位了。难道真的是它飘了?要不我改回去?(改回去结果当然还是正常的哈哈哈哈哈)
既然是多线程不安全,兄弟们,那就上锁!于是乎代码变成了这样
public class SingleThreadWriteFileDemo {
private static final Logger logger = LoggerFactory.getLogger(SingleThreadWriteFileDemo.class);
private static final String content = "I am a java coder, i am strong!\n";
public static void main(String[] args) throws InterruptedException {
twoThreadFileWriter();
}
private static void twoThreadFileWriter() {
Stream.of(1,2).forEach(i -> {
new Thread(SingleThreadWriteFileDemo::fileWriter).start();
});
}
public static void fileWriter() {
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("test.txt", true)))) {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
try {
writer(writer);
} catch (IOException e) {
e.printStackTrace();
}
}
printDuration(start);
} catch (IOException e) {
e.printStackTrace();
}
}
// 把写内容的方法单拎出来,加了个锁
private static synchronized void write(BufferedWriter writer) throws IOException {
writer.write(content);
}
}
哼,加了锁,你还能给我错位?走你!
再次打开 test.txt ,同样的位置,熟悉的配方,还是222行错位了。
等下,多线程并发问题不一向以随机著称吗?为什么这次错位的位置还是222行?难道它真的这么2?
我不相信,于是又跑了几遍,错位的位置还是在222行
奇了怪了,到底是什么魔法,可以让它错得那么始终如一
忽然,不知道是天意还是开窍了,我将错位之前的字符全部选中,也就是从第一行到第222行 I am a java cod 的所有字符,这时候idea给出的字符数是8192!
8192?8192?8192?这个数字怎么这么熟悉?我肯定在哪里见过!
啊!这不就是BufferedWriter的默认缓冲区大小吗?
那这个大小又跟这错位位置有什么关系呢?
不妨我们去看看BufferedWriter的源码,看它到底在鬼鬼祟祟做什么?
public class BufferedWriter extends Writer {
public void write(String s, int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();
int b = off, t = off + len;
while (b < t) {
int d = min(nChars - nextChar, t - b);
s.getChars(b, b + d, cb, nextChar);
b += d;
nextChar += d;
if (nextChar >= nChars)
flushBuffer();
}
}
}
}
哟吼,没想到一进来它自己就给上了把锁
然当缓冲区满了之后,就把缓冲区的字符写到文件中
我马上打开计算器计算一波
缓冲区是8192,而一行 ```I am a java coder, i am very strong!\n```是37个字符(包括空格和换行符)
8197/37=221余15
所以写了完整的221行
第222行就是余的这15个字符: I am a java cod
这还不是问题的关键,关键是缓冲区一刷,它就把锁给释放了
锁一释放,问题就大了
还有个老6(线程2)在虎视眈眈地盯着这把锁呢
注意这把锁不是 BufferedWriter的锁,是 FileOutputStream 的锁
它抢到了这把锁之后,看到自己的缓冲区也满了,二话不说就把自己的缓冲区刷了先
这下真的是书接上文了,只不过接得不大好看
问题整明白了,那咋解决啊?
我简单粗暴地加上了一句 writer.flush();
public class SingleThreadWriteFileDemo {
private static final Logger logger = LoggerFactory.getLogger(SingleThreadWriteFileDemo.class);
private static final String content = "I am a java coder, i am strong!\n";
public static void main(String[] args) throws InterruptedException {
twoThreadFileWriter();
}
private static void twoThreadFileWriter() {
Stream.of(1,2).forEach(i -> {
new Thread(SingleThreadWriteFileDemo::fileWriter).start();
});
}
public static void fileWriter() {
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("test.txt", true)))) {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
try {
write(writer);
} catch (IOException e) {
e.printStackTrace();
}
}
printDuration(start);
} catch (IOException e) {
e.printStackTrace();
}
}
// BufferedWriter里面有锁了,就把synchronized关键字给删了
private static void write(BufferedWriter writer) throws IOException {
writer.write(content);
// 直接把
writer.flush();
}
}
原本以为这样做就可以让那句没有完整输出的语句给刷出来
但是细想一下,这么一操作,不是每次 writer.write(String)
都把缓存给刷到磁盘了吗?
那我还要这 Buffer 有啥用?
那 BufferedWriter 有什么方法可以知道缓存满了吗?
当它要刷缓存的时候,顺带帮我把那句被截断话的一起给刷出去
但是很遗憾,确认过眼神之后,不是对的人,并没有这样的方法