Java并發(fā)基礎(chǔ)實(shí)踐--退出任務(wù)I
計(jì)劃寫一個"Java并發(fā)基礎(chǔ)實(shí)踐"系列,算作本人對Java并發(fā)學(xué)習(xí)與實(shí)踐的簡單總結(jié)。本文是該系列的第一篇,介紹了退出并發(fā)任務(wù)的最簡單方法。(2013.09.25最后更新)在一個并發(fā)任務(wù)被啟動之后,不要期望它總是會執(zhí)行完成。由于時間限制,資源限制,用戶操作,甚至是任務(wù)中的異常(尤其是運(yùn)行時異常),...都可能造成任務(wù)不能執(zhí)行完成。如何恰當(dāng)?shù)赝顺鋈蝿?wù)是一個很常見的問題,而且實(shí)現(xiàn)方法也不一而足。
1. 任務(wù)
創(chuàng)建一個并發(fā)任務(wù),遞歸地獲取指定目錄下的所有子目錄與文件的絕對路徑,最后再將這些路徑信息保存到一個文件中,如代碼清單1所示:
清單1
public class FileScanner implements Runnable {
private File root = null;
private List<String> filePaths = new ArrayList<String>();
public FileScanner1(File root) {
if (root == null || !root.exists() || !root.isDirectory()) {
throw new IllegalArgumentException("root must be legal directory");
}
this.root = root;
}
@Override
public void run() {
travleFiles(root);
try {
saveFilePaths();
} catch (Exception e) {
e.printStackTrace();
}
}
private void travleFiles(File parent) {
String filePath = parent.getAbsolutePath();
filePaths.add(filePath);
if (parent.isDirectory()) {
File[] children = parent.listFiles();
for (File child : children) {
travleFiles(child);
}
}
}
private void saveFilePaths() throws IOException {
FileWriter fos = new FileWriter(new File(root.getAbsoluteFile()
+ File.separator + "filePaths.out"));
for (String filePath : filePaths) {
fos.write(filePath + "\n");
}
fos.close();
}
}
public class FileScanner implements Runnable {
private File root = null;
private List<String> filePaths = new ArrayList<String>();
public FileScanner1(File root) {
if (root == null || !root.exists() || !root.isDirectory()) {
throw new IllegalArgumentException("root must be legal directory");
}
this.root = root;
}
@Override
public void run() {
travleFiles(root);
try {
saveFilePaths();
} catch (Exception e) {
e.printStackTrace();
}
}
private void travleFiles(File parent) {
String filePath = parent.getAbsolutePath();
filePaths.add(filePath);
if (parent.isDirectory()) {
File[] children = parent.listFiles();
for (File child : children) {
travleFiles(child);
}
}
}
private void saveFilePaths() throws IOException {
FileWriter fos = new FileWriter(new File(root.getAbsoluteFile()
+ File.separator + "filePaths.out"));
for (String filePath : filePaths) {
fos.write(filePath + "\n");
}
fos.close();
}
}
2. 停止線程
有一個很直接,也很干脆的方式來停止線程,就是調(diào)用Thread.stop()方法,如代碼清單2所示:
清單2
public static void main(String[] args) throws Exception {
FileScanner task = new FileScanner(new File("C:"));
Thread taskThread = new Thread(task);
taskThread.start();
TimeUnit.SECONDS.sleep(1);
taskThread.stop();
}
但是,地球人都知道Thread.stop()在很久很久之前就不推薦使用了。根據(jù)官方文檔的介紹,該方法存在著固有的不安全性。當(dāng)停止線程時,將會釋放該線程所占有的全部監(jiān)視鎖,這就會造成受這些鎖保護(hù)的對象的不一致性。在執(zhí)行清單2的應(yīng)用程序時,它的運(yùn)行結(jié)果是不確定的。它可能會輸出一個文件,其中包含部分的被掃描過的目錄和文件。但它也很有可能什么也不輸出,因?yàn)樵趫?zhí)行FileWriter.write()的過程中,可能由于線程停止而造成了I/O異常,使得最終無法得到輸出文件。public static void main(String[] args) throws Exception {
FileScanner task = new FileScanner(new File("C:"));
Thread taskThread = new Thread(task);
taskThread.start();
TimeUnit.SECONDS.sleep(1);
taskThread.stop();
}
3. 可取消的任務(wù)
另外一種十分常見的途徑是,在設(shè)計(jì)之初,我們就使任務(wù)是可被取消的。一般地,就是提供一個取消標(biāo)志或設(shè)定一個取消條件,一旦任務(wù)遇到該標(biāo)志或滿足了取消條件,就會結(jié)束任務(wù)的執(zhí)行。如代碼清單3所示:
清單3
public class FileScanner implements Runnable {
private File root = null;
private List<String> filePaths = new ArrayList<String>();
private boolean cancel = false;
public FileScanner(File root) {

}
@Override
public void run() {

}
private void travleFiles(File parent) {
if (cancel) {
return;
}
String filePath = parent.getAbsolutePath();
filePaths.add(filePath);
if (parent.isDirectory()) {
File[] children = parent.listFiles();
for (File child : children) {
travleFiles(child);
}
}
}
private void saveFilePaths() throws IOException {

}
public void cancel() {
cancel = true;
}
}
新的FileScanner實(shí)現(xiàn)提供一個cancel標(biāo)志,travleFiles()會遍歷新的文件之前檢測該標(biāo)志,若該標(biāo)志為true,則會立即返回。代碼清單4是使用新任務(wù)的應(yīng)用程序。public class FileScanner implements Runnable {
private File root = null;
private List<String> filePaths = new ArrayList<String>();
private boolean cancel = false;
public FileScanner(File root) {

}
@Override
public void run() {

}
private void travleFiles(File parent) {
if (cancel) {
return;
}
String filePath = parent.getAbsolutePath();
filePaths.add(filePath);
if (parent.isDirectory()) {
File[] children = parent.listFiles();
for (File child : children) {
travleFiles(child);
}
}
}
private void saveFilePaths() throws IOException {

}
public void cancel() {
cancel = true;
}
}
清單4
public static void main(String[] args) throws Exception {
FileScanner task = new FileScanner(new File("C:"));
Thread taskThread = new Thread(task);
taskThread.start();
TimeUnit.SECONDS.sleep(3);
task.cancel();
}
但有些時候使用可取消的任務(wù),并不能快速地退出任務(wù)。因?yàn)槿蝿?wù)在檢測取消標(biāo)志之前,可能正處于等待狀態(tài),甚至可能被阻塞著。對清單2中的FileScanner稍作修改,讓每次訪問新的文件之前先睡眠10秒鐘,如代碼清單5所示:public static void main(String[] args) throws Exception {
FileScanner task = new FileScanner(new File("C:"));
Thread taskThread = new Thread(task);
taskThread.start();
TimeUnit.SECONDS.sleep(3);
task.cancel();
}
清單5
public class FileScanner implements Runnable {

private void travleFiles(File parent) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (cancel) {
return;
}

}
private void saveFilePaths() throws IOException {

}
public void cancel() {
cancel = true;
}
}
再執(zhí)行清單3中的應(yīng)用程序時,可能發(fā)現(xiàn)任務(wù)并沒有很快速的退出,而是又等待了大約7秒鐘才退出。如果在檢查cancel標(biāo)志之前要先獲取某個受鎖保護(hù)的資源,那么該任務(wù)就會被阻塞,并且無法確定何時能夠退出。對于這種情況,就需要使用中斷了。public class FileScanner implements Runnable {

private void travleFiles(File parent) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (cancel) {
return;
}

}
private void saveFilePaths() throws IOException {

}
public void cancel() {
cancel = true;
}
}
4. 中斷
中斷是一種協(xié)作機(jī)制,它并不會真正地停止一個線程,而只是提醒線程需要被中斷,并將線程的中斷狀態(tài)設(shè)置為true。如果線程正在執(zhí)行一些可拋出InterruptedException的方法,如Thread.sleep(),Thread.join()和Object.wait(),那么當(dāng)線程被中斷時,上述方法就會拋出InterruptedException,并且中斷狀態(tài)會被重新設(shè)置為false。任務(wù)程序只要恰當(dāng)處理該異常,就可以正常地退出任務(wù)。對清單5再稍作修改,即,如果任務(wù)在睡眠時遇上了InterruptedException,那么就取消任務(wù)。如代碼清單6所示:
清單6
public class FileScanner implements Runnable {

private void travleFiles(File parent) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
cancel();
}
if (cancel) {
return;
}

}

}
同時將清單4中的應(yīng)用程序,此時將調(diào)用Thread.interrupt()方法去中斷線程,如代碼清單7所示:public class FileScanner implements Runnable {

private void travleFiles(File parent) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
cancel();
}
if (cancel) {
return;
}

}

}
清單7
public static void main(String[] args) throws Exception {
FileScanner3 task = new FileScanner3(new File("C:"));
Thread taskThread = new Thread(task);
taskThread.start();
TimeUnit.SECONDS.sleep(3);
taskThread.interrupt();
}
或者更進(jìn)一步,僅使用中斷狀態(tài)來控制程序的退出,而不再使用可取消的任務(wù)(即,刪除cancel標(biāo)志),將清單6中的FileScanner修改成如下:public static void main(String[] args) throws Exception {
FileScanner3 task = new FileScanner3(new File("C:"));
Thread taskThread = new Thread(task);
taskThread.start();
TimeUnit.SECONDS.sleep(3);
taskThread.interrupt();
}
清單8
public class FileScanner implements Runnable {

private void travleFiles(File parent) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (Thread.currentThread().isInterrupted()) {
return;
}

}

}
再次執(zhí)行清單7的應(yīng)用程序后,新的FileScanner也能即時的退出了。值得注意的是,因?yàn)楫?dāng)sleep()方法拋出InterruptedException時,該線程的中斷狀態(tài)將又會被設(shè)置為false,所以必須要再次調(diào)用interrupt()方法來保存中斷狀態(tài),這樣在后面才可以利用中斷狀態(tài)來判定是否需要返回travleFiles()方法。當(dāng)然,對于此處的例子,在收到InterruptedException時也可以選擇直接返回,如代碼清單9所示:public class FileScanner implements Runnable {

private void travleFiles(File parent) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (Thread.currentThread().isInterrupted()) {
return;
}

}

}
清單9
public class FileScanner implements Runnable {

private void travleFiles(File parent) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
return;
}

}

}
public class FileScanner implements Runnable {

private void travleFiles(File parent) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
return;
}

}

}
5 小結(jié)
本文介紹了三種簡單的退出并發(fā)任務(wù)的方法:停止線程;使用可取消任務(wù);使用中斷。毫無疑問,停止線程是不可取的。使用可取消的任務(wù)時,要避免任務(wù)由于被阻塞而無法及時,甚至永遠(yuǎn)無法被取消。一般地,恰當(dāng)?shù)厥褂弥袛嗍侨∠蝿?wù)的首選方式。
posted on 2013-09-21 19:11 John Jiang 閱讀(2049) 評論(0) 編輯 收藏 所屬分類: JavaSE 、Java 、Concurrency 、原創(chuàng) 、Java并發(fā)基礎(chǔ)實(shí)踐