1. 程式人生 > >多執行緒讀寫檔案的安全

多執行緒讀寫檔案的安全

以前負責一個專案,我負責從一個超大的文字檔案中讀取資訊存入資料庫再進一步分析。而文字檔案內容是每行一個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條中的執行緒安全,寫入速度下降超過一半。