最近发现一个问题,对某个地市全月的批价话单进行重批号码过滤时,把所有重批号码的话单多线程都用BufferedWriter写入一个文件Filter.txt,后来通过统计日志发现日志中打印的过滤话单总数和Filter.txt的总数不一致,虽然不影响生产结果,但是让我百思不得其解,又重新怀疑BufferedWriter的线程安全性。
首先怀疑的是BufferedWriter.flush方法的线程安全性造成刷新到磁盘时出现覆盖,于是去看源码
public voidflush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}
--可以看到,flush是线程安全的。排除
然后怀疑是否是缓冲区太小了,导致部分数据写入缓冲区时丢失,因为写入Filter.txt文件的记录数达到几亿条,于是自我感觉这个可能性比较大,于是去看了相关源码
private static int defaultCharBufferSize =8192; //BufferedWriter类的属性缓冲区的大小8K
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize);
}
public BufferedWriter(Writer out, int sz) {
super(out);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.out = out;
cb = new char[sz];
nChars = sz;
nextChar = 0;
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}
--该大小数据通过构造方法传到BufferedWriter类的属性nChars,这个属性在write中很关键
Write方法源码中有这么一段
if (len >= nChars)
flushBuffer();
out.write(cbuf, off, len);
什么意思?就是但缓冲区的数据长度将大于设定的长度时就进行涮洗数据到磁盘
所以这个原因也被排除,其实很正常,jdk不可能犯这么低级的错误,正常读写文件大小远远大于8K,如果因为缓冲区大小不够造成数据丢失,这种低级的错误是不能在jdk出现的。
最后终于发现了真正原因:那就是在过滤程序跑的时候,重批号码话单提取程序也在2个小时后启动,由于这两个程序都是我编写的,当时为了方便,两个功能就放在通过java文件中实现,只是最后打包时分别调用不同的功能方法,但是一些初始化方法是共用的,比如BufferedWriter对象的创建:FilterWriter = new BufferedWriter(newFileWriter("Filter.txt"));
也就是说上面的代码隔两个小时后执行了两次,所以就出现了后面一次执行直接把前面两个小时过滤话单得写入记录给置空,所以是少了这部分过滤记录,然后上去more Filter.txt发现果然前面一大段都是空的,由于过滤程序的BufferedWriter对象中记录了写文件时的偏移量,所以直接往后面写过滤记录,所以后面那段是有记录的。通过这次bug的排查,也学到了些知识点,比如察看FileWriter的一部分源码,构造方法:
public FileWriter(String fileName) throwsIOException {
super(new FileOutputStream(fileName));
}
public OutputStreamWriter(OutputStream out){
super(out);
try {
se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
--可以发现其实FileWriter是封装了OutputStream,这样就把字符流和字节流联系起来了,某种程度来说,字符流底层是使用了字节流的能力的。
另外
super(new FileOutputStream(fileName));
里面的new FileOutputStream(fileName)的实现
public FileOutputStream(String name) throwsFileNotFoundException {
this(name != null ? new File(name) : null, false);
}
--里面用到了new File(name),File类,这样就把字节流,字符流和文件File类这三块联系起来了,关于这三者的联系,后续再深入分析,现在初步怀疑,BufferedWriter里面的文件偏移量来自于File类的偏移量属性PREFIX_LENGTH_OFFSET