Java多线程学习之线程的取消与中断机制

  任务和线程的启动很容易。在大多数情况下我们都会让他们运行直到结束,或是让他们自行停止。但是,有时我们希望提前结束任务或是线程,可能是因为用户请求取消,或是线程在规定时间内没有结束,或是出现了一些问题迫使线程要提前结束。

  强制一个线程或是服务立即停止,可能会造成共享数据状态不一致的问题,比如,两个线程正对一个共享数据进行操作,然后被突然杀死,这样会对数据造成不确定性的影响。Java中没有提供任何机制来安全的终止线程,但它提供了中断,这种协作机制,“提醒”线程可以自己结束自己线程。这种机制提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清楚工作。

1、使用“标志”变量取消任务

 1 public class PrimeGenerator implements Runnable {
 2     private final List<BigInteger> primes = new ArrayList<>();
 3     // 标志变量,设置为volatile,保证可见性
 4     private volatile boolean canceled = false;
 5     @Override
 6     public void run() {
 7         BigInteger p = BigInteger.ONE;
 8         // 依靠标志位判断是否结束线程
 9         while(!canceled){
10             p = p.nextProbablePrime();
11             synchronized (this){
12                 primes.add(p);
13             }
14         }
15     }
16     // 取消
17     public void cancel(){canceled = true;}
18     //返回结果
19     public synchronized List<BigInteger> get(){
20         return primes;
21     }
22 }

  上述代码设置一个volatile “已请求取消”标志,而任务将定期查看该标志。 PrimeGenerator 将持续的枚举素数,直到标志位被设置为取消结束。PrimeGenerator  每次枚举素数时候都会检查canceled标志位是否被改变。

 1 public List<BigInteger> aPrimes() throws InterruptedException {
 2         PrimeGenerator generator = new PrimeGenerator();
 3         new Thread(generator).start();
 4         try{
 5             // 睡眠1秒
 6             TimeUnit.SECONDS.sleep(1);
 7         }finally {
 8             // 1秒后取消
 9             generator.cancel();
10         }
11         return generator.get();
12 }

  调用素数生成器运行1秒后取消,值得注意的是,素数生成器可能不会在1秒后“准时”停止,因为他可能此时刚好在while内执行。取消语句放在finally语句执行,保证该语句一定会被执行。

2、取消策略

  在设计良好的程序中,一个可取消的任务必须拥有取消策略,这个策略详细定义取消操作的“How”、“When”、“What”,即代码如何(How)请求取消该任务,任务在何时(When)检查是否已经请求了取消,以及在响应时执行那些(What)操作。

  在上述代码中,PrimeGenerator采用了简单的取消策略:客户代码通过canceled来请求取消,PrimeGenerator在每次执行搜索前首先检查是否存在取消请求,如果存在则退出。

3、中断线程

  PrimeGenerator 中取消机制之所以能成功,是因为程序会不间断定期的检查标志位的状态是否被改变。但是,如果程序调用了一个阻塞方法,例如,BlockingQueu.put()那么可能会出现问题,即任务可能永远不会检查取消标志。【阻塞队列不了解的看看这篇博客:http://www.cnblogs.com/moongeek/p/7832855.html#_label3

 1 // 不推荐的写法
 2 public class BrokenPrimeProducer extends Thread {
 3     // 阻塞队列
 4     private final BlockingQueue<BigInteger> queue;
 5     // 中断位
 6     private volatile boolean canceled = false;
 7
 8     public BrokenPrimeProducer(BlockingQueue<BigInteger> queue){
 9         this.queue = queue;
10     }
11
12     @Override
13     public void run(){
14         try {
15             BigInteger p = BigInteger.ONE;
16             while (!canceled) {
17               // PUT操作可能会被阻塞,将无法检查 canceled 是否变化,因而无法响应退出
18                 queue.put(p = p.nextProbablePrime());
19             }
20         }catch (InterruptedException ex){}
21     }
22
23     public void cancel(){
24         canceled = true;
25     }
26 }

  如果阻塞队列在 put()  操作被阻塞,此时,即使我们调用cancel() 方法将状态变量改变,进程也无法检查到改变,因为会一直阻塞下去。

  每个Thread都有一个boolean类型的中断状态。当中断线程时,改状态会被置为true。Thread中包含的中断方法如下。其中 inturrept() 会将中断状态置为true,而 isInterrupted() 方法会返回当前的中断状态,而 interrupted() 方法则会清除当前状态,并返回它之前的值。

1 public class Thread{
2     public void inturrept(){......}
3       public boolean isInterrupted(){......}
4       public static boolean interrupted(){......}
5 }

  通常情况下,如果一个阻塞方法,如:Object.wait()、Thread.sleep()Thread.join() 时,都会去检查中断状态的值,发现中断状态变化时都会提前返回并响应中断:清除中断状态,并抛出InterruptedException异常

  该注意的是,中断操作并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由程序在合适的时刻中断自己。一般设计方法时,都需要捕获到中断异常后对中断请求进行某些操作,不能完全忽视或是屏蔽中断请求。

  对上代码进行改进,采用中断进行中断程序执行。代码中有两处可以检测中断:在阻塞的put() 方法中,以及循环开始处的查询中断状态时。其实put() 操作会检测响应异常,在循环开始时可以不进行检测,但这样可以获得更高效的响应性能。

 1 public class PrimeProducer extends Thread {
 2     // 阻塞队列
 3     private final BlockingQueue<BigInteger> queue;
 4
 5     public PrimeProducer(BlockingQueue<BigInteger> queue){
 6         this.queue = queue;
 7     }
 8
 9     @Override
10     public void run(){
11         try {
12             BigInteger p = BigInteger.ONE;
13             while (!Thread.currentThread().isInterrupted()) {
14                 queue.put(p = p.nextProbablePrime());
15             }
16         }catch (InterruptedException ex){
17             // 允许退出线程
18         }
19     }
20
21     public void cancel(){
22         // 中断
23         interrupt();
24     }
25 }

  中断是实现取消的最合理方式,在取消之外的其他操作中使用中断,都是不合理的。

4、中断策略

  中断策略解释某个中断请求:当发现中断请求时,应该做哪些工作,以多快的速度来响应中断。任务一般不会在其自己拥有的线程中执行,而是在其他某个服务(比如说,在一个其他线程或是线程池)中执行。对于非线程所有者而言(例如,对线程池来说,任何线程池实现之外的代码),应该保存并传递中断状态,使得真正拥有线程的代码才能对中断做出响应。

  比如说,如果你书写一个库函数,一般会抛出InterruptedException作为中断响应,而不会在库函数时候把中断异常捕获并进行提前处理,而导致调用者被屏蔽中断。因为你不清楚调用者想要对异常进行何种处理,比如说,是接收中断后立即停止任务还是进行相关处理并继续执行任务。中断的处理必须由该任务自己决定,而不是由其他线程决定。

  因为在捕获InterruptException 中会同时把中断位恢复,所以,如果想捕获异常后恢复中断位,一般会调用 Thread.currentThread.interrupt() 进行中断位的恢复。

1  try {
2     // dosomething();
3  } catch (InterruptedException e) {
4     // 捕获异常后恢复中断位
5    Thread.currentThread().interrupt();
6    e.printStackTrace();
7  }

5、使用Future 来实现取消

  关于Future 对象:ExecutorService.submit 方法将返回一个Future 来描述任务。

 1 public interface Future<V> {
 2     // 是否取消线程的执行
 3     boolean cancel(boolean mayInterruptIfRunning);
 4     // 线程是否被取消
 5     boolean isCancelled();
 6     //线程是否执行完毕
 7     boolean isDone();
 8       // 立即获得线程返回的结果
 9     V get() throws InterruptedException, ExecutionException;
10       // 延时时间后再获得线程返回的结果
11     V get(long timeout, TimeUnit unit)
12         throws InterruptedException, ExecutionException, TimeoutException;
13 }
 1 public static void main(String[] args) {
 2         ExecutorService service = Executors.newSingleThreadExecutor();
 3         Future future = service.submit(new TheradDemo());
 4
 5         try {
 6           // 可能抛出异常
 7             future.get();
 8         } catch (InterruptedException e) {
 9             e.printStackTrace();
10         } catch (ExecutionException e) {
11             e.printStackTrace();
12         }finally {
13           //终止任务的执行
14             future.cancel(true);
15         }
16  }

  Future 中的  cancel(boolean mayInterruptIfRunning) 接受一个布尔参数表示取消操作是否成功。如果Future.get()  抛出异常,如果你不需要得到结果时,就可以通过cancel(boolean) 来取消任务。

  对于线程池中的任务,如果想想要取消执行某任务,不宜中断线程池,因为你不知道中断请求到达时正在执行什么任务,所以只能通过cancel(boolean) 来定向取消特定的任务。

6、关闭ExecutorService

  线程池相关对象ExecutorService 提供了两种关闭的方法:使用 shutdown() 正常关闭,他先把线程池状态设置为SHUTDOWN ,禁止再向线程池提交任务,然后把线程池中的任务全部执行完毕,就关闭线程池。这种方法速度较慢,但是更安全。以及使用shutdownNow() 首先关闭正在执行的任务,然后返回所有尚未启动的任务清单。这种方法速度快,但风险也大,因为有的任务可能执行了一般被关闭。

时间: 2024-10-13 02:50:10

Java多线程学习之线程的取消与中断机制的相关文章

Java多线程学习之线程的状态及中断线程

线程的状态 新建(new):当线程被创建时,它只会短时间处于这种状态.它已经分配了必要的系统资源,完成了初始化.之后线程调度器将把这个线程转变为可运行或者阻塞状态: 就绪(Runnable):在这种状态下,只要调度器分配时间片给线程,线程就可以运行了: 阻塞(Blocked):有某个条件阻止线程运行,调度器将忽略阻塞状态的线程,不会分配时间片给它,直到线程进入就绪状态,它才有可能执行: 死亡(Dead):处于死亡或者终结状态的线程将不再是可调度的,并且也不会被分配到时间片.任务死亡的方式通常是从

Java多线程学习之线程的同步

多线程编程要解决的一个基本问题是:共享资源的竞争.而基本上使用并发模式在解决这个问题都采用序列化访问共享资源的方法.基本原理就是当共享资源被一个任务使用时,在其上加锁,其他任务在资源被解锁之前,无法访问它.在任务对其解锁后,另一个任务就可以锁定并使用它.下面看看Java支持的线程同步机制. 1.synchronized关键字 synchronized关键字即可应用于对象相关的同步,也可用于类层次的同步(static属性): 对象上应用synchronized可以实现对象方法的同步和代码块的同步.

java多线程学习(四)——线程的交互

线程交互中用到的三个基本函数: void notify():唤醒在此对象监视器上等待的单个线程. void notifyAll():唤醒在此对象监视器上等待的所有线程. void wait();导致当前的线程等待,直到其他线程调用此对象的notify()或者notifyAll()方法. void wait(long timeout);wait()的重载版本,同样导致当前线程等待,直到其他线程调用此对象的notify()或者notifyAll()方法,或者等待超过指定的时间后不再等待. void

java多线程学习(2)

1)Callable和Future Runnable封装一个异步运行的任务:可以当成一个没有任何参数和返回值的异步方法,Callable和 Runnable类似,但是它有返回值和参数. Callable接口是一个参数化的类型,只有一个方法call. 1 public interface Callable<V> 2 3 { 4 5 V call()throws Exception; 6 7 } 类型参数v是指返回值的类型,例如Callable<Integer>代表最终返回一个Inte

java多线程学习--java.util.concurrent

CountDownLatch,api 文档:http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/CountDownLatch.html A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. 假设我们要打印1-100,最

java多线程学习(3)

1)竞争条件 在实际的多线程应用中,通常会有两个或多个线程需要对共同的对象进行共享访问,如果两个线程访问相同的对象,而且每一个都调用了一个会改变对象状态的方法, 那么,线程就会相互倾轧.根据各个线程访问数据的不同顺序,可能会产生腐蚀现象.这种情况通常称为竞争条件. 2)同步 为了多个线程对共享数据的腐蚀,就需要对数据的存取实现同步:常用的同步方法有3种: 1.Reenlock 用Reenlock保护代码块的基本机构如下: 1 Lock myLock=new ReenLock; 2 3 myLoc

java多线程学习(1)

1)多线程与多进程的区别 多线程和多进程有什么区别呢?本质的区别在于每个进程有它自己的变量的完备集,线程则共享相同的数据. 对程序来说,共享的变量会使得线程之间的通信比进程间的通信更加有效和简单:同时,线程相对于进程来说,更加的“轻量级”, 线程的创建和销毁要比 进程的 开销要小的多. 2)多线程程序的构造 多线程的构造通常有两种方法, 第一种方法是,构建一个Thread的子类,并重写它的run()方法: 1 class MyThread extends Thread 2 { 3 4 publi

java SE学习之线程同步(详细介绍)

       java程序中可以允许存在多个线程,但在处理多线程问题时,必须注意这样一个问题:               当两个或多个线程同时访问同一个变量,并且一些线程需要修改这个变量时,那么这个程序是该如何执行?              也就是先访问这个变量还是先修改这个变量.              在学习线程的这段时间里,我也一直被这个问题所困扰!但是今天终于算是搞明白了.              于是将这些好的列子一一列举出来,分享一下. (1)什么是线程同步 ?      

java基础学习总结——线程(二)

永不放弃,一切皆有可能!!! 只为成功找方法,不为失败找借口! java基础学习总结——线程(二) 一.线程的优先级别 线程优先级别的使用范例: 1 package cn.galc.test; 2 3 public class TestThread6 { 4 public static void main(String args[]) { 5 MyThread4 t4 = new MyThread4(); 6 MyThread5 t5 = new MyThread5(); 7 Thread t1