Java并发基础实践--退出任务I(原)
Java并发基础实践--退出任务I
计划写一个"Java并发基础实践"系列,算作本人对Java并发学习与实践的简单总结。本文是该系列的第一篇,介绍了退出并发任务的最简单方法。(2013.09.25最后更新)
在一个并发任务被启动之后,不要期望它总是会执行完成。由于时间限制,资源限制,用户操作,甚至是任务中的异常(尤其是运行时异常),...都可能造成任务不能执行完成。如何恰当地退出任务是一个很常见的问题,而且实现方法也不一而足。
1. 任务
创建一个并发任务,递归地获取指定目录下的所有子目录与文件的绝对路径,最后再将这些路径信息保存到一个文件中,如代码清单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. 停止线程
有一个很直接,也很干脆的方式来停止线程,就是调用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()在很久很久之前就不推荐使用了。根据官方文档的介绍,该方法存在着固有的不安全性。当停止线程时,将会释放该线程所占有的全部监视锁,这就会造成受这些锁保护的对象的不一致性。在执行清单2的应用程序时,它的运行结果是不确定的。它可能会输出一个文件,其中包含部分的被扫描过的目录和文件。但它也很有可能什么也不输出,因为在执行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. 可取消的任务
另外一种十分常见的途径是,在设计之初,我们就使任务是可被取消的。一般地,就是提供一个取消标志或设定一个取消条件,一旦任务遇到该标志或满足了取消条件,就会结束任务的执行。如代码清单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实现提供一个cancel标志,travleFiles()会遍历新的文件之前检测该标志,若该标志为true,则会立即返回。代码清单4是使用新任务的应用程序。
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();
}
但有些时候使用可取消的任务,并不能快速地退出任务。因为任务在检测取消标志之前,可能正处于等待状态,甚至可能被阻塞着。对清单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 ;
}
}
再执行清单3中的应用程序时,可能发现任务并没有很快速的退出,而是又等待了大约7秒钟才退出。如果在检查cancel标志之前要先获取某个受锁保护的资源,那么该任务就会被阻塞,并且无法确定何时能够退出。对于这种情况,就需要使用中断了。
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. 中断
中断是一种协作机制,它并不会真正地停止一个线程,而只是提醒线程需要被中断,并将线程的中断状态设置为true。如果线程正在执行一些可抛出InterruptedException的方法,如Thread.sleep(),Thread.join()和Object.wait(),那么当线程被中断时,上述方法就会抛出InterruptedException,并且中断状态会被重新设置为false。任务程序只要恰当处理该异常,就可以正常地退出任务。对清单5再稍作修改,即,如果任务在睡眠时遇上了InterruptedException,那么就取消任务。如代码清单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中的应用程序,此时将调用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();
}
或者更进一步,仅使用中断状态来控制程序的退出,而不再使用可取消的任务(即,删除cancel标志),将清单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 ;
}
}
}
再次执行清单7的应用程序后,新的FileScanner也能即时的退出了。值得注意的是,因为当sleep()方法抛出InterruptedException时,该线程的中断状态将又会被设置为false,所以必须要再次调用interrupt()方法来保存中断状态,这样在后面才可以利用中断状态来判定是否需要返回travleFiles()方法。当然,对于此处的例子,在收到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 小结
本文介绍了三种简单的退出并发任务的方法:停止线程;使用可取消任务;使用中断。毫无疑问,停止线程是不可取的。使用可取消的任务时,要避免任务由于被阻塞而无法及时,甚至永远无法被取消。一般地,恰当地使用中断是取消任务的首选方式。