多執行緒讀寫檔案的安全
阿新 • • 發佈:2019-02-04
以前負責一個專案,我負責從一個超大的文字檔案中讀取資訊存入資料庫再進一步分析。而文字檔案內容是每行一個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條中的執行緒安全,寫入速度下降超過一半。
2 //同時向同一個檔案寫入字串,每個執行緒每次寫一行。
3 //測試結果:檔案內容出現混亂,序列 4 private void multiThreadWriteFile() throws IOException{
5 File file=new
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
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條中的執行緒安全,寫入速度下降超過一半。