John Jiang

a cup of Java, cheers!
https://github.com/johnshajiang/blog

   :: 首页 ::  :: 联系 :: 聚合  :: 管理 ::
  131 随笔 :: 1 文章 :: 530 评论 :: 0 Trackbacks
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();
    }
}

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异常,使得最终无法得到输出文件。

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是使用新任务的应用程序。
清单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所示:
清单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标志之前要先获取某个受锁保护的资源,那么该任务就会被阻塞,并且无法确定何时能够退出。对于这种情况,就需要使用中断了。

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所示:
清单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修改成如下:
清单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所示:
清单9
public class FileScanner implements Runnable {

    

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

        
    }

    
}

5 小结
本文介绍了三种简单的退出并发任务的方法:停止线程;使用可取消任务;使用中断。毫无疑问,停止线程是不可取的。使用可取消的任务时,要避免任务由于被阻塞而无法及时,甚至永远无法被取消。一般地,恰当地使用中断是取消任务的首选方式。
posted on 2013-09-21 19:11 John Jiang 阅读(1995) 评论(0)  编辑  收藏 所属分类: JavaSEJavaConcurrency原创Java并发基础实践

只有注册用户登录后才能发表评论。


网站导航: