以前負責一個項目,我負責從一個超大的文本文件中讀取信息存入數據庫再進一步分析。而文本文件內容是每行一個json串。我在解析的過程中發現,有很小的概率json串的結構會破壞,比如前一個json串只寫了半行,后面就被另一個json串覆蓋掉了。
與產生日志的部門溝通,他們說是多線程使用log4j寫入,可能偶爾會有串行。
具體他們是否使用log4j的AsyncAppender我不太了解,暫時也沒去看log4j的源碼,當時只是簡單的忽略異常的行了事兒。
現在比較閑,想測試一下jdk里面各種輸出方式,例如Writer,在多線程交替寫入文件一行時是否會出現串行的情況,于是便出現了本文。
測試分兩部分:
1,多個線程各自開啟一個FileWriter寫入同一個文件。
2,多個線程共用一個FileWriter寫入同一個文件。
--------------------------------------------------
首先來看FileWriter的JDK說明:
“某些平臺一次只允許一個 FileWriter(或其他文件寫入對象)打開文件進行寫入”——如果是這樣,那么第1個測試便不用做了,可事實上至少在windows下并非如此。
上代碼(別嫌丑,咱是在IO,不是在測多線程,您說是吧?):
1,多個線程各自開啟一個FileWriter寫入同一個文件。
1 //在100毫秒的時間內,10個線程各自開一個FileWriter,
2 //同時向同一個文件寫入字符串,每個線程每次寫一行。
3 //測試結果:文件內容出現混亂,串行
4 private void multiThreadWriteFile() throws IOException{
5 File file=new File(basePath+jumpPath+fileName);
6 file.createNewFile();
7
8 //創建10個線程
9 int totalThreads=10;
10 WriteFileThread[] threads=new WriteFileThread[totalThreads];
11 for(int i=0;i<totalThreads;i++){
12 WriteFileThread thread=new WriteFileThread(file,i);
13 threads[i]=thread;
14 }
15
16 //啟動10個線程
17 for(Thread thread: threads){
18 thread.start();
19 }
20
21 //主線程休眠100毫秒
22 try {
23 Thread.sleep(100);
24 } catch (InterruptedException e) {
25 e.printStackTrace();
26 }
27
28 //所有線程停止
29 for(WriteFileThread thread: threads){
30 thread.setToStop();
31 }
32 System.out.println("還楞著干什么,去看一下文件結構正確與否啊!");
33 }
1 class WriteFileThread extends Thread{
2 private boolean toStop=false;
3 private FileWriter writer;
4 private int threadNum;
5 private String lineSeparator;
6
7 WriteFileThread(File file,int threadNum) throws IOException{
8 lineSeparator=System.getProperty("line.separator");
9 writer=new FileWriter(file,true);
10 this.threadNum=threadNum;
11 }
12
13 @Override
14 public void run() {
15 while(!toStop){
16 try {
17 writer.append("線程"+threadNum+"正在寫入文件," +
18 "媽媽說名字要很長才能夠測試出這幾個線程有沒有沖突啊," +
19 "不過還是沒有論壇里帖子的名字長,怎么辦呢?" +
20 "哎呀,后面是換行符了"+lineSeparator);
21
22 } catch (IOException e) {
23 e.printStackTrace();
24 }
25 }
26 System.out.println("---------線程"+threadNum+"停止執行了");
27 }
28
29 public void setToStop() {
30 this.toStop = true;
31 }
32 }
測試結果:
產生5MB左右的文本文件,里面出現大約5%的文本串行現象。
--------------------------------------------------
接下來我們看多個線程共用一個FileWriter寫入同一個文件的情況:
在Writer抽象類里面有一個protected類型的lock屬性,是一個簡單Object對象。
JDK里對這個lock屬性的描述如下:“用于同步針對此流的操作的對象。為了提高效率,字符流對象可以使用其自身以外的對象來保護關鍵部分。因此,子類應使用此字段中的對象,而不是 this 或者同步的方法。 ”——看來,多線程共用同一個writer的方案有戲。
繼續看下源代碼,從FileWriter的writer方法開始看起,調用過程如下:
FileWriter->OutputStreamWriter.write->StreamEncoder.write
其中StreamEncoder.write的源碼如下:
(JDK自帶源碼不包括StreamExcoder,可以在這里查看 http://www.docjar.com/html/api/sun/nio/cs/StreamEncoder.java.html)
1 public void write(char cbuf[], int off, int len) throws IOException {
2 synchronized (lock) {
3 ensureOpen();
4 if ((off < 0) || (off > cbuf.length) || (len < 0) ||
5 ((off + len) > cbuf.length) || ((off + len) < 0))
6 {
7 throw new IndexOutOfBoundsException();
8 } else if (len == 0) {
9 return;
10 }
11 implWrite(cbuf, off, len);
12 }
13 }
可以看到FileWriter在寫入時,同步在了對應的FileOutputStream對象上——依此分析,多個線程共用一個FileWriter寫入同一個文件,一次一行的情況下,不會出現串行。
寫代碼測試一下:
1 //多線程爭搶寫入同一個文件的測試,一次一行
2 //多個線程公用一個FileWriter
3 //測試結果:
4 private void multiThreadWriteFile2() throws IOException{
5 File file=new File(basePath+jumpPath+fileName);
6 file.createNewFile();
7 FileWriter fw=new FileWriter(file);
8
9 //創建10個線程
10 int totalThreads=10;
11 WriteFileThread2[] threads=new WriteFileThread2[totalThreads];
12 for(int i=0;i<totalThreads;i++){
13 WriteFileThread2 thread=new WriteFileThread2(fw,i);
14 threads[i]=thread;
15 }
16
17 //啟動10個線程
18 for(Thread thread: threads){
19 thread.start();
20 }
21
22 //主線程休眠100毫秒
23 try {
24 Thread.sleep(100);
25 } catch (InterruptedException e) {
26 e.printStackTrace();
27 }
28
29 //所有線程停止
30 for(WriteFileThread2 thread: threads){
31 thread.setToStop();
32 }
33 System.out.println("還楞著干什么,去看一下文件結構正確與否啊!");
34 }
1 class WriteFileThread2 extends Thread{
2 private boolean toStop=false;
3 private FileWriter writer;
4 private int threadNum;
5 private String lineSeparator;
6
7 WriteFileThread2(FileWriter writer,int threadNum){
8 lineSeparator=System.getProperty("line.separator");
9 this.writer=writer;
10 this.threadNum=threadNum;
11 }
12
13 @Override
14 public void run() {
15 while(!toStop){
16 try {
17 writer.append("線程"+threadNum+"正在寫入文件," +
18 "媽媽說名字要很長才能夠測試出這幾個線程有沒有沖突啊," +
19 "不過還是沒有論壇里帖子的名字長,怎么辦呢?" +
20 "哎呀,后面是換行符了"+lineSeparator);
21 } catch (IOException e) {
22 e.printStackTrace();
23 }
24 }
25 System.out.println("---------線程"+threadNum+"停止執行了");
26 }
27
28 public void setToStop() {
29 this.toStop = true;
30 }
31 }
測試結果:
產生2.2MB左右的文本文件,里面沒有出現任何串行現象。
--------------------------------------------------
那么BufferedWriter又如何呢?
按道理BufferedWriter只是把別的Writer裝飾了一下,在底層寫的時候也是同步的。
看源碼:
1 void flushBuffer() throws IOException {
2 synchronized (lock) {
3 ensureOpen();
4 if (nextChar == 0)
5 return;
6 out.write(cb, 0, nextChar);
7 nextChar = 0;
8 }
9 }
BufferedWriter.write和BufferedWriter.flushBuffer的方法同步在了被包裝的Writer這個對象上。
也就是說,BufferedWriter.write和BufferedWriter.flushBuffer都有同步塊包圍,說明按上述環境測試時,是不會出現串行現象的。
--------------------------------------------------
最終結果:
1,windows下,可以開多個線程操作多個FileWriter寫入同一個文件,多個FileWriter切換時,會導致相互交錯,破壞字符串結構的完整性。
2,多個線程操作FileWriter或者BufferedWriter時,每一次寫入操作都是可以保證原子性的,也即:FileWriter或者BufferedWriter是線程安全的——呃,這個結論貌似好簡單啊,JDK文檔里有說明嗎?沒看到啊。
3,由于第2條中的線程安全,寫入速度下降超過一半。