使用單獨的線程在 Java 中讀寫文件
一、簡介
當涉及到 Java 中的文件處理時,管理大文件而不引起效能問題可能具有挑戰性。這就是使用單獨線程的概念的由來。透過使用單獨的線程,我們可以在不阻塞主線程的情況下有效地讀寫文件。在本教程中,我們將探討如何使用單獨的執行緒讀取和寫入檔案。
2. 為什麼要使用單獨的線程
使用單獨的執行緒進行檔案操作可以透過允許並發執行任務來提高效能。在單執行緒程式中,檔案操作是依序執行的。例如,我們先讀取整個文件,然後寫入另一個文件。這可能非常耗時,尤其是對於大檔案。
透過使用單獨的線程,可以同時執行多個檔案操作,利用多核心處理器以及將 I/O 操作與計算重疊。這種並發性可以更好地利用系統資源並減少整體執行時間。但是,必須注意的是,使用單獨執行緒的有效性取決於任務的性質和所涉及的 I/O 操作。
3. 使用執行緒實作文件操作
可以使用單獨的線程來讀取和寫入檔案以提高效能。在本節中,我們將討論如何使用執行緒來實作文件操作。
3.1.在單獨的線程中讀取文件
要在單獨的線程中讀取文件,我們可以創建一個新線程並傳遞一個讀取文件的Runnable
物件。 FileReader
類別用於讀取檔案。此外,為了增強文件讀取過程,我們使用BufferedReader
來有效率地逐行讀取文件:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread.start();
3.2.在單獨的線程中寫入文件
我們建立另一個新執行緒並使用FileWriter
類別將資料寫入檔案:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try (FileWriter fileWriter = new FileWriter(filePath)) {
fileWriter.write("Hello, world!");
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread.start();
這種方法允許讀取和寫入同時運行,這意味著它們可以在單獨的執行緒中同時發生。當一項操作不依賴另一項操作的完成時,這尤其有用。
4. 處理並發
多個執行緒對檔案的並發存取需要特別注意,以避免資料損壞和意外行為。在前面的程式碼中,兩個執行緒是同時啟動的。這意味著兩者可以同時執行,並且不能保證它們的操作交錯的順序。如果讀取器執行緒在寫入操作仍在進行時嘗試存取文件,則可能最終會讀取不完整或部分寫入的資料。這可能會導致處理過程中出現誤導性資訊或錯誤,這可能會影響依賴準確數據的下游操作。
此外,如果兩個寫入執行緒同時嘗試將資料寫入文件,它們的寫入可能會交錯並覆蓋彼此的部分資料。如果沒有正確的同步處理,這可能會導致資訊損壞或不一致。
為了解決這個問題,一個常見的方法是使用生產者-消費者模型。一個或多個生產者執行緒讀取檔案並將其新增至佇列中,一個或多個消費者執行緒處理佇列中的檔案。這種方法使我們能夠根據需要添加更多生產者或消費者來輕鬆擴展我們的應用程式。
5. 使用BlockingQueue
進行並發檔案處理
帶有隊列的生產者-消費者模型協調操作,確保讀寫順序一致。為了實作這個模型,我們可以使用執行緒安全的佇列資料結構,例如BlockingQueue
。生產者可以使用offer()
方法將檔案加入佇列中,消費者可以使用poll()
方法檢索檔案。
每個BlockingQueue
實例都有一個內部鎖,用於管理對其內部資料結構(鍊錶、陣列等)的存取。當執行緒嘗試執行像offer()
或poll()
這樣的操作時,它首先會取得此鎖。這可確保一次只有一個執行緒可以存取佇列,從而防止同時修改和資料損壞。
透過使用BlockingQueue
,我們將生產者和消費者解耦,允許他們按照自己的步調工作,而無需直接相互等待。這可以提高整體效能。
5.1.建立FileProducer
我們首先建立FileProducer
類,代表負責從輸入檔案讀取行並將其新增至共用佇列的生產者執行緒。此類利用BlockingQueue
在生產者執行緒和消費者執行緒之間進行協調。它接受BlockingQueue
作為行的同步存儲,確保消費者線程可以存取它們。
以下是FileProducer
類別的範例:
class FileProducer implements Runnable {
private final BlockingQueue<String> queue;
private final String inputFileName;
public FileProducer(BlockingQueue<String> queue, String inputFileName) {
this.queue = queue;
this.inputFileName = inputFileName;
}
// ...
}
接下來,在run()
方法中,我們使用BufferedReader
開啟檔案以進行高效率的行讀取。我們還包括對檔案操作期間可能發生的潛在IOException
的錯誤處理。
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(new FileReader(inputFileName))) {
String line;
// ...
} catch (IOException e) {
e.printStackTrace();
}
}
打開檔案後,程式碼進入循環,從檔案讀取行並同時使用offer()
方法將它們新增至佇列:
while ((line = reader.readLine()) != null) {
queue.offer(line);
}
5.2.建立FileConsumer
接下來,我們介紹FileConsumer
類,它代表消費者線程,其任務是從佇列中檢索行並將其寫入輸出檔案。此類別接受BlockingQueue
作為從生產者執行緒接收行的輸入:
class FileConsumer implements Runnable {
private final BlockingQueue<String> queue;
private final String outputFileName;
public FileConsumer(BlockingQueue queue, String outputFileName) {
this.queue = queue;
this.outputFileName = outputFileName;
}
// ...
}
接下來,在run()
方法中我們使用BufferedWriter
來促進高效寫入輸出檔案:
@Override
public void run() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFileName))) {
String line;
// ...
} catch (IOException e) {
e.printStackTrace();
}
}
開啟輸出檔案後,程式碼進入連續循環,使用poll()
方法從佇列中擷取行。如果某行可用,則將該行寫入檔案。當poll()
傳回null
時,循環終止,這表示生產者已完成寫入行並且沒有更多行需要處理:
while ((line = queue.poll()) != null) {
writer.write(line);
writer.newLine();
}
5.3.執行緒編排器
最後,我們將所有內容包裝在主程式中。首先,我們建立一個LinkedBlockingQueue
實例作為生產者執行緒和消費者執行緒之間線路的中介。此佇列建立了用於通訊和協調的同步通道。
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
接下來,我們建立兩個線程:一個FileProducer
線程,負責從輸入檔案讀取行並將它們新增到佇列中。我們還創建一個FileConsumer
線程,其任務是從隊列中檢索行並熟練地處理它們的處理並將其輸出到指定的輸出檔案:
String fileName = "input.txt";
String outputFileName = "output.txt"
Thread producerThread = new Thread(new FileProducer(queue, fileName));
Thread consumerThread = new Thread(new FileConsumer(queue, outputFileName);
隨後,我們使用 start() 方法啟動它們的執行。我們利用join()
方法來確保兩個執行緒在程式退出之前正常完成其工作:
producerThread.start();
consumerThread.start();
try {
producerThread.join();
consumerThread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
現在,讓我們建立一個輸入文件,然後運行該程式:
Hello,
Baeldung!
Nice to meet you!
運行程式後,我們可以檢查輸出檔。我們應該看到輸出檔包含與輸入檔相同的行:
Hello,
Baeldung!
Nice to meet you!
在提供的範例中,生產者在循環中將行添加到佇列中,而消費者在循環中從佇列中檢索行。這意味著隊列中可以同時存在多行,並且即使生產者仍在添加更多行,消費者也可以處理隊列中的行。
六,結論
在本文中,我們探討如何利用單獨的執行緒在 Java 中進行高效率的文件處理。我們也示範了使用BlockingQueue
來實現檔案的同步高效的逐行處理。
與往常一樣,範例的原始程式碼可在 GitHub 上取得。