现在的位置: 首页 > 综合 > 正文

Java并发系列(六)任务的执行、取消与关闭

2013年02月15日 ⁄ 综合 ⁄ 共 9943字 ⁄ 字号 评论关闭
文章目录

Author:Martin

E-mail:mwdnjupt@sina.com.cn

CSDN Blog:http://blog.csdn.net/ictcamera

Sina MicroBlog ID:ITCamera

Main Reference:

《Java并发编程实战》 Brian Goetz etc 童云兰等译

《Java并发设计教程》 温绍锦

1.        任务的执行

大多数并发应用程序都是围绕“任务执行”来构造的,把应用程序的工作分解到多个任务中,可以简化程序的组织结构,并且提供一定的并发性。而java中的任务最终都在线程中完成(脱离不了线程)。

1.1.        任务和线程概述

1、 单线程串行地执行任务:在单线程中串行地执行任务是最简单的并发处理策略,显然这种处理并发性很差,在实际应用中很少用到(几乎不用)。如果将这个策略用在服务端来响应多客户端的并发请求,那么这种服务器在高并发情况下几乎不可用。

2、 显式的为每任务创建一个线程:这种策略实施也非常简单,也具有很好的并发性能,但是在高并发请求的情况下,由于任务过多导致线程泛滥,因为无限制的创建线程是十分危险的,这种情况下线程生命周期的开销非常高资源消耗非常大稳定性也变差

3、 线程池:管理任务的线程资源池,加上工作队列,实现任务调度和线程的管理。这种设计可以实现任务执行在并发和性能上的平衡。Java类库中的Executors框架(包括Executor、Callable、Future等)就提供了这种功能。

4、 Timer:实现定时和延迟执行任务,Timer在执行所有定时任务时会创建一个线程,如果时间过长会影响定时的精度。

1.2.        任务调度开源工具

Quartz:Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的日程序表。Jobs可以做成标准的Java组件或 EJBs。Quartz在大型项目中应用较多(这里job就是任务)

2.        任务的取消与关闭

当我们点击某个杀毒软件的取消按钮来停止查杀病毒时,当我们在控制台敲入quit命令以结束某个后台服务时……都需要通过一个线程去取消另一个线程正在执行的任务。主要包括以下几大类:

l  用户请求取消

l  有时间限制的操作,超时的时候取消

l  应用程序事件,例如一组任务中一个任务完成,其他任务取消

l  错误,任务执行中发生错误(IO异常等)

l  关闭,关闭一个程序或服务

Java中取消可以通过程序设置取消标识(变量),然后根据执行任务前判断变量来达到取消,但是这种设计在实际应用基本上不使用,因为阻塞的任务处理可能是取消严重滞后或者永远查不到取消标识而永远不会结束。实际中一般通过Java的中断机制来实现任务的关闭和取消。中断是实现取消和关闭的最合理方式

2.1.        中断概述

Java没有提供一种安全、直接的方法来停止某个线程,但是Java提供了中断(interrupt)机制。如果我们对Java中断没有一个全面的了解,可能会误以为被中断的线程将立马退出运行,但事实并非如此。

Java中断机制是一种协作机制,这个和通常所指的计算机系统的中断没有任何关系,含义也是完全不一样。通过Java中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。java.lang.Thread类提供了几个方法来操作这个中断状态,这些方法包括:

public static boolean interrupted()

测试当前线程是否已经中断。线程的中断状态 (中断标识)由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

public boolean isInterrupted ()

测试线程是否已经中断。线程的中断状态 不受该方法的影响。

public void interrupt ()

中断线程。由中断调用者调用,调用该方法会将被中断线程的中断状态设置为true

2.2.        可中断方法

    调用线程对象的interrupt方法并不一定就中断了正在运行的线程,它只是要求线程自己在合适的时机中断自己。每个线程都有一个boolean的中断状态,interrupt方法仅仅只是将该状态置为true。先看下面的例子,例子中虽然调用了线程的interrupt但是线程实际没有被中断:

publicclass CanNotInterrupted

{

   
publicstaticvoid main(String[] args)

   
{

       
Thread t = new MyThread();

       
t.start();

       
t.interrupt();

       
System.out.println("已调用线程的interrupt方法");

   
}

   
staticclass MyThreadextends Thread

   
{

       
publicvoid run()

       
{

           int num =longTimeRunningNonInterruptMethod(3, 0);

           System.out.println("长时间任务运行结束,num="
+ num);

           System.out.println("线程的中断状态:"
+ Thread.interrupted());

       
}

       
privatestaticint longTimeRunningNonInterruptMethod(int
count,
int initNum)

       
{

           for(int
i = 0; i < count; i++)

           {

               for(int
j = 0; j < count; j++)

               {

                   initNum++;

               }

           }

           return initNum;

       
}

   
}

}

输出结果如下:

已调用线程的interrupt方法

长时间任务运行结束,num=9

线程的中断状态:true

可见,调用interrupt方法并不一定能中断线程。但是,如果改成下面的程序,情况会怎样呢?

publicclass CanInterrupted

{

   
publicstaticvoid main(String[] args) {

       
Thread t = new MyThread();

       
t.start();

       
t.interrupt();

    
   System.out.println("已调用线程的interrupt方法"
);

   
}

   
staticclass MyThreadextends Thread {

       
publicvoid run() {

           int num = - 1 ;

           try {

               num =
longTimeRunningNonInterruptMethod( 2 , 0 );

           }
catch (InterruptedException e) {

               System.out.println("线程被中断"
);

               thrownew RuntimeException(e);

           }

           System.out.println("长时间任务运行结束,num="
+ num);

           System.out.println("线程的中断状态:"
+ Thread.interrupted());

       
}

       
privatestaticint longTimeRunningNonInterruptMethod(int
count,

                       int initNum)throws
InterruptedException

       
{

           for(int
i = 0; i < count; i++)

           {

               for(int
j = 0; j < count; j++)

               {

                   initNum++;

               }

               TimeUnit.SECONDS.sleep( 5 );

           }

           return initNum;

       
}

   
}

}

输出结果如下:

Exception in thread "Thread-0"java.lang.RuntimeException:java.lang.InterruptedException:
sleep interrupted

      
at org.martin.interrupt.CanInterrupted$MyThread

.run(CanInterrupted.java:20)

Caused by:
java.lang.InterruptedException: sleep interrupted

      
at java.lang.Thread.sleep(Native Method)

      
at java.lang.Thread.sleep(Unknown Source)

      
at java.util.concurrent.TimeUnit.sleep(Unknown Source)

      
at org.martin.interrupt.CanInterrupted$MyThread

.longTimeRunningNonInterruptMethod(CanInterrupted.java:34)at org.martin.interrupt.CanInterrupted$MyThread

.run(CanInterrupted.java:17)

上面的例子说明调用interrupt方法线程被中断了。那么什么情况能被中断,什么情况又不能中断?说明这个问题前先说明三个概念:

l  中断调用者——调用interrupt()方法的线程,当然线程调用者也可以是被中断线程自身;

l  被中断线程——被调用interrupt()方法的线程;

l  可中断方法——在被调用interrupt()方法后,线程运行中的可中断方法,会对中断作中断响应(处理);

l  不可中断方法——与可中断方相反,比如等待内置锁、阻塞IO操作等等就不会对中断做任何处理。

可中断方法包括:声明抛出InterruptedException异常的方法的低级阻塞方法,如BlockingQueue的put、BlockingQueue的take、Object的wait()、Thread.join()、Thread的sleep等等,其中Thread的sleep方法,检测到线程的中状态为true,那么该方法会清除中断状态(将中断状态改为false),抛出InterruptedException。如果interrupt调用是在可中断方法之前调用,可中断方法一定会处理中断,像上面的例子,interrupt方法极可能在run中未进入sleep的时候就调用了,
sleep检测到中断,就是处理该中断。如果在可中断方法正在执行中的时候调用interrupt,会怎么样呢?这就要看可中断方法处理中断的时机了,只要可中断方法能检测到中断状态为true,就应该处理中断。一般可中断方法都会声明抛出InterruptedException异常(当然没有在方法中处理中断却也声明抛出InterruptedException的除外)。

    那么是否可以自定义可中断方法来响应(处理)中断呢?该如何响应(处理)中断呢?答案肯定是可以的,我们要做的就是在适合处理中断的地方检测线程中断状态并进行响应(处理)。如下面所示:

publicclass ManualInterrupted

{

   
publicstaticvoid main(String[] args)throws
Exception {

       
Thread t = new MyThread();

       
t.start();

       
t.interrupt();

       
System.out.println("已调用线程的interrupt方法"
);

   
}

   
staticclass MyThreadextends Thread {

       
publicvoid run() {

           int num;

           try {

               num =
longTimeRunningNonInterruptMethod( 2 , 0 );

           }
catch (InterruptedException e) {

    
           System.out.println("线程被中断" );

               thrownew RuntimeException(e);

           }

           System.out.println("长时间任务运行结束,num="
+ num);

           System.out.println("线程的中断状态:"
+ Thread.interrupted());

       
}

       
privatestaticint longTimeRunningNonInterruptMethod(int
count,

                                int initNum)throws
InterruptedException {

           if (interrupted()) {

               thrownew InterruptedException("正式处理前线程已经被请求中断"
);

           }

           for(int
i = 0; i < count; i++)

           {

               for(int
j = 0; j < count; j++)

               {

                   initNum++;

               }

               //假如这就是一个合适的地方

               if (interrupted()) {

                   //回滚数据,清理操作等,中断标识已经被清除

                   thrownew InterruptedException("线程正在处理过程中被中断"
);

               }

           }

           return initNum;

       
}

   
}

}

输出如下所示

已调用线程的interrupt方法

线程被中断

Exception in thread "Thread-0"java.lang.RuntimeException:java.lang.InterruptedException:正式处理前线程已经被请求中断

      
at org.martin.interrupt.ManualInterrupted$MyThread.run(ManualInterrupted.java:21)

Caused by:
java.lang.InterruptedException:正式处理前线程已经被请求中断

      
at org.martin.interrupt.ManualInterrupted$MyThread.longTimeRunningNonInterruptMethod(ManualInterrupted.java:29)

      
at org.martin.interrupt.ManualInterrupted$MyThread.run(ManualInterrupted.java:18)

如上面的代码,方法longTimeRunningNonInterruptMethod此时已是一个自定义的可中断的方法。在进入方法的时候判断是否被请求中断,如果是,就不进行相应的处理了;处理过程中,可能也有合适的地方处理中断,例如上面最内层循环结束后。这段代码中检测中断用了Thread的静态方法interrupted,它将中断状态置为false,并将之前的状态返回,而isInterrupted只是检测中断,并不改变中断状态。一般来说,处理过了中断请求,应该将其状态置为false,但具体还要看实际情形。

2.3.        可中断方法的中断响应(处理)

如果程序捕获到这些可中断的阻塞方法抛出的InterruptedException或在自定义可中断方法中检测到中断状态后,这些中断信息该如何处理?一般有以下两个通用原则:

如果遇到的是可中断的阻塞方法抛出InterruptedException,可以继续向方法调用栈的上层抛出该异常,如果是检测到中断状态,则可清除中断状态并抛出InterruptedException,使当前方法也成为一个可中断的方法。

若有时候不太方便在方法上抛出InterruptedException,比如要实现的某个接口中的方法签名上没有throws InterruptedException,这时就可以捕获可中断方法的InterruptedException并通过Thread.currentThread.interrupt()来重新设置中断状态。自定义可中断方法也是如此。

类库中的有些类的方法也可能会调用中断,如FutureTask中的cancel方法,如果传入的参数为true,它将会在正在运行异步任务的线程上调用interrupt方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么cancel方法中的参数将不会起到什么效果;又如ThreadPoolExecutor中的shutdownNow方法会遍历线程池中的工作线程并调用线程的interrupt方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。

程序里发现中断后该怎么响应?这就得视实际情况而定了。有些程序可能一检测到中断就立马将线程终止,有些可能是退出当前执行的任务,继续执行下一个任务……作为一种协作机制,这要与中断方协商好,当调用interrupt会发生些什么都是事先知道的,如做一些事务回滚操作,一些清理工作,一些补偿操作等。若不确定调用某个线程的interrupt后该线程会做出什么样的响应,那就不应当中断该线程

2.4.        不可中断方法的响应(处理)

比如等待内置锁、阻塞IO操作等等就不会对中断做任何处理,中断只会修改线程的中断标识,不会改变线程阻塞的状态。如果要达到如可中断的效果,应该搞清楚阻塞的原因,然后调用方法改变阻塞状态使其返回,一般这种方法都会抛出异常,这是后我们应该捕获异常,并且将线程中断状态设置为true。例如下面所示:

   publicclass ReaderThreadextends
Thread {

        privatefinal Socketsocket;

        privatefinal InputStreamin;

        public ReaderThread(Socket socket)throws
IOException{

            this.socket=socket;

            this.in=socket.getInputStream();

        }

        publicvoid interrupt(){

            try{

                socket.close();

            }catch(IOException ignored){

               

            }finally{

                super.interrupt();

            }

        }

        publicvoid run(){

            try{

                byte[] buf=newbyte[BUFSZ];

                while(true){

                    int count=in.read(buf);

                    if(count<0){

                        break;

                    }elseif(count>0){

                        processBuffer(buffer,count);

                    }

                }

            }catch(IOException e){

                //允许线程退出

            }

        }

    }

 

2.5.        中断策略

任务应该包含一定的取消、关闭策略,同样线程也要包含中断策略,要规定线程如何解释某个中断请求,即当发现中断请求时,应该做哪些工作(如果需要的话),哪些工作单元对于中断来说是原子性的,以及多块的速度来响应中断等问题。任务执行代码不应该对其执行所在的线程的中断策略作出假设,而应该以线程的拥有者对中断作出决定,而且除非知道中断对该线程的确切含义,否则不应该中断这个线程。

作为一种协作机制,中断不会强求被中断线程一定要在某个点进行处理。实际上,被中断线程只需在合适的时候处理即可,如果没有合适的时间点,甚至可以不处理,这时候在任务处理层面,就跟没有调用中断方法一样。“合适的时候”与线程正在处理的业务逻辑紧密相关,例如,每次迭代的时候,进入一个可能阻塞且无法中断的方法之前等,但多半不会出现在某个临界区更新另一个对象状态的时候,因为这可能会导致对象处于不一致状态。

处理时机决定着程序的效率与中断响应的灵敏性。频繁的检查中断状态可能会使程序执行效率下降,相反,检查的较少可能使中断请求得不到及时响应。如果发出中断请求之后,被中断的线程继续执行一段时间不会给系统带来灾难,那么就可以将中断处理放到方便检查中断的同时又能从一定程度上保证响应灵敏度的地方。当程序的性能指标比较关键时,可能需要建立一个测试模型来分析最佳的中断检测点,以平衡性能和响应灵敏性。

2.6.        中断的使用场景

通常,中断的使用场景有以下几个:

点击某个桌面应用中的取消按钮时;

某个操作超过了一定的执行时间限制需要中止时;

多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;

一组线程中的一个或多个出现错误导致整组都无法继续时;

当一个应用或服务需要停止时。

2.7.        Thread.interrupt和Thread.stop

Thread.stop方法已经不推荐使用了。而在某些方面Thread.stop与中断机制有着相似之处。如当线程在等待内置锁或IO时,stop跟interrupt一样,不会中止这些操作;当catch住stop导致的异常时,程序也可以继续执行,虽然stop本意是要停止线程,这么做会让程序行为变得更加混乱。那么它们的区别在哪里?最重要的就是中断需要程序自己去检测然后做相应的处理,而Thread.stop会直接在代码执行过程中抛出ThreadDeath错误,这是一个java.lang.Error的子类。正是因为可能导致对象状态不一致,stop才被禁用,这里只是作简单的比较。

抱歉!评论已关闭.