Java并发基础实践--退出任务I(原)

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();
    }
}

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 小结
本文介绍了三种简单的退出并发任务的方法:停止线程;使用可取消任务;使用中断。毫无疑问,停止线程是不可取的。使用可取消的任务时,要避免任务由于被阻塞而无法及时,甚至永远无法被取消。一般地,恰当地使用中断是取消任务的首选方式。

你可能感兴趣的:(Java并发基础实践--退出任务I(原))