深入分析Java线程中断机制

简介:

Thread.interrupt真的能中断线程吗

在平时的开发过程中,相信都会使用到多线程,在使用多线程时,大家也会遇到各种各样的问题,今天我们就来说说一个多线程的问题——线程中断。在 java中启动线程非常容易,大多数情况下我是让一个线程执行完自己的任务然后自己停掉,但是有时候我们需要取消某个操作,比如你在网络下载时,有时候需 要取消下载。实现线程的安全中断并不是一件容易的事情,因为Java并不支持安全快速中断线程的机制,这里估计很多同学就会说了,java不是提供了Thread.interrupt 方法中断线程吗,好吧,我们今天就从这个方法开始说起。

深入分析Java线程中断机制

但是调用此方法线程真的会停止吗?我们写个demo看看就知道了。


 
 
  1. public class Main { 
  2.   private static final String TAG = "Main"
  3.   public static void main(String[] args) { 
  4.     Thread t=new Thread(new NRunnable()); 
  5.     t.start(); 
  6.     System.out.println("is start......."); 
  7.     try { 
  8.       Thread.sleep(3000); 
  9.     } catch (InterruptedException e) { 
  10.  
  11.     } 
  12.  
  13.     t.interrupt(); 
  14.     System.out.println("is interrupt......."); 
  15.  
  16.   } 
  17.  
  18.   public static class NRunnable implements Runnable 
  19.   { 
  20.  
  21.     @Override 
  22.     public void run() { 
  23.       while(true
  24.       { 
  25.         System.out.println("我没有种中断"); 
  26.         try { 
  27.           Thread.sleep(1000); 
  28.         } catch (InterruptedException e) { 
  29.  
  30.         } 
  31.       } 
  32.     } 
  33.  
  34.   } 

如果interrutp方法能够中断线程,那么在打印了is interrupt…….之后应该是没有log了,我们看看执行结果吧

is start.......
我没有种中断
我没有种中断
我没有种中断
我没有种中断
我没有种中断
is interrupt.......
我没有种中断
我没有种中断
我没有种中断
我没有种中断
我没有种中断
....

通过结果可以发现子线程并没有中断

所以 Thread.interrupt() 方法并不能中断线程,该方法仅仅告诉线程外部已经有中断请求,至于是否中断还取决于线程自己。在Thread类中除了interrupt() 方法还有另外两个非常相似的方法:interrupted 和 isInterrupted 方法,下面来对这几个方法进行说明:

  • interrupt 此方法是实例方法,用于告诉此线程外部有中断请求,并且将线程中的中断标记设置为true

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

  • isInterrupted 此方法是实例方法测试线程是否已经中断。线程的中断状态 不受该方法的影响。 线程中断被忽略,因为在中断时不处于活动状态的线程将由此返回 false 的方法反映出来

处理线程中断的常用方法

设置取消标记

还是用上面的例子,只不过做了些修改


 
 
  1. public static void main(String[] args) { 
  2.     NRunnable run=new NRunnable(); 
  3.     Thread t=new Thread(run); 
  4.     t.start(); 
  5.     System.out.println("is start......."); 
  6.     try { 
  7.       Thread.sleep(3000); 
  8.     } catch (InterruptedException e) { 
  9.  
  10.     } 
  11.     run.cancel(); 
  12.     System.out.println("cancel ..."+System.currentTimeMillis()); 
  13.   } 
  14.  
  15.   public static class NRunnable implements Runnable 
  16.   { 
  17.     public boolean isCancel=false
  18.  
  19.     @Override 
  20.     public void run() { 
  21.       while(!isCancel) 
  22.       { 
  23.         System.out.println("我没有种中断"); 
  24.         try { 
  25.           Thread.sleep(10000); 
  26.         } catch (InterruptedException e) { 
  27.  
  28.         } 
  29.       } 
  30.       System.out.println("我已经结束了..."+System.currentTimeMillis()); 
  31.     } 
  32.  
  33.     public void cancel() 
  34.     { 
  35.       this.isCancel=true
  36.     } 
  37.  
  38.   } 

执行结果如下:

is start.......
我没有种中断
cancel ...1438396915809
我已经结束了...1438396922809

通过结果,我们发现线程确实已经中断了,但是细心的同学应该发现了一个问题,调用cancel方法和最后线程执行完毕之间隔了好几秒的时间,也就是说线程不是立马中断的,我们下面来分析一下原因:

子线程退出的条件是while循环结束,也就是cancel标示设置为true,但是当我们调用cancel方法将calcel标记设置为true 时,while循环里面有一个耗时操作(sleep方法模拟),只有等待耗时操作执行完毕后才会去检查这个标记,所以cancel方法和线程退出中间有时 间间隔。

通过interrupt 和 isinterrupt 方法来中断线程


 
 
  1. public static void main(String[] args) { 
  2.     Thread t=new NThread(); 
  3.     t.start(); 
  4.     System.out.println("is start......."); 
  5.     try { 
  6.       Thread.sleep(3000); 
  7.     } catch (InterruptedException e) { 
  8.  
  9.     } 
  10.     System.out.println("start interrupt..."+System.currentTimeMillis()); 
  11.     t.interrupt(); 
  12.     System.out.println("end interrupt ..."+System.currentTimeMillis()); 
  13.   } 
  14.  
  15.   public static class NThread extends Thread 
  16.   { 
  17.  
  18.     @Override 
  19.     public void run() { 
  20.       while(!this.isInterrupted()) 
  21.       { 
  22.         System.out.println("我没有种中断"); 
  23.         try { 
  24.           Thread.sleep(10000); 
  25.         } catch (InterruptedException e) { 
  26.           Thread.currentThread().interrupt(); 
  27.         } 
  28.       } 
  29.       System.out.println("我已经结束了..."+System.currentTimeMillis()); 
  30.     } 
  31.  
  32.   } 

运行结果如下:

is start.......
我没有种中断
start interrupt...1438398800110
我已经结束了...1438398800110
end interrupt ...1438398800110

这次是立马中断的,但是这种方法是由局限性的,这种方法仅仅对于会抛出InterruptedException 异常的任务时有效的,比如java中的sleep、wait 等方法,对于不会抛出这种异常的任务其效果其实和第一种方法是一样的,都会有延迟性,这个例子中还有一个非常重要的地方就是cache语句中,我们调用了Thread.currentThread().interrupt() 我们把这句代码去掉,运行你会发现这个线程无法终止,因为在抛出InterruptedException 的同时,线程的中断标志被清除了,所以在while语句中判断当前线程是否中断时,返回的是false.针对InterruptedException 异常,我想说的是:一定不能再catch语句块中什么也不干,如果你实在不想处理,你可以将异常抛出来,让调用抛异常的方法也成为一个可以抛出InterruptedException 的方法,如果自己要捕获此异常,那么最好在cache语句中调用 Thread.currentThread().interrupt(); 方法来让高层只要中断请求并处理该中断。

对于上述两种方法都有其局限性,第一种方法只能处理那种工作量不大,会频繁检查循环标志的任务,对于第二种方法适合用于抛出InterruptedException的代码。也就是说第一种和第二种方法支持的是支持中断的线程任务,那么不支持中断的线程任务该怎么做呢。

例如 如果一个线程由于同步进行I/O操作导致阻塞,中断请求不会抛出InterruptedException ,我们该如何中断此线程呢。

处理不支持中断的线程中断的常用方法

改写线程的interrupt方法


 
 
  1. public static class ReaderThread extends Thread 
  2.    public static final int BUFFER_SIZE=512
  3.    Socket socket; 
  4.    InputStream is; 
  5.  
  6.    public ReaderThread(Socket socket) throws IOException 
  7.    { 
  8.      this.socket=socket; 
  9.      is=this.socket.getInputStream(); 
  10.    } 
  11.  
  12.    @Override 
  13.   public void interrupt() { 
  14.      try 
  15.      { 
  16.        socket.close(); 
  17.      }catch(IOException e) 
  18.      { 
  19.  
  20.      }finally 
  21.      { 
  22.        super.interrupt(); 
  23.      } 
  24.     super.interrupt(); 
  25.   } 
  26.    @Override 
  27.   public void run() { 
  28.      try 
  29.      { 
  30.        byte[]buf=new byte[BUFFER_SIZE]; 
  31.        while(true
  32.        { 
  33.          int count=is.read(buf); 
  34.          if(count<0
  35.            break
  36.          else if(count>0
  37.          { 
  38.  
  39.          } 
  40.        } 
  41.      }catch(IOException e) 
  42.      { 
  43.  
  44.      } 
  45.   } 

例如在上面的例子中,改写了Thread的interrupt 方法,当调用interrupt 方法时,会关闭socket,如果此时read方法阻塞,那么会抛出IOException 此时线程任务也就结束了。

以上方法是通过改写线程的interrupt 方法实现,那么对于使用线程池的任务该怎么中断呢。

改写线程池的newTaskFor方法

通常我们向线程池中加入一个任务采用如下形式:


 
 
  1. Future<?> future=executor.submit(new Runnable(){ 
  2.       @Override 
  3.       public void run() { 
  4.  
  5.       } 
  6.     }); 
  7.  
  8. 取消任务时,调用的是future的cancel方法,其实在cancel方法中调用的是线程的interrupt方法。所以对于不支持中断的任务cancel也是无效的,下面我们看看submit方法里面干了上面吧 
  9.  
  10.     public Future<?> submit(Runnable task) { 
  11.         if (task == nullthrow new NullPointerException(); 
  12.         RunnableFuture<Void> ftask = newTaskFor(task, null); 
  13.         execute(ftask); 
  14.         return ftask; 
  15.     } 
  16.  
  17. 这里调用的是AbstractExecutorService 的newTaskFor方法,那么我们能不能改写ThreadPoolExecutor的newTaskFor方法呢,接下来看我在处理吧 
  18.  
  19. 定义一个基类,所有需要取消的任务继承这个基类 
  20.  
  21. public interface CancelableRunnable<T> extends Runnable { 
  22.  
  23.   public void cancel(); 
  24.   public RunnableFuture<T> newTask(); 
  25.  
  26.  
  27. 将上面的ReaderThread改为继承这个类 
  28.  
  29.  public static class ReaderThread implements CancelableRunnable<Void> 
  30.   { 
  31.     public static final int BUFFER_SIZE=512
  32.     Socket socket; 
  33.     InputStream is; 
  34.  
  35.     public ReaderThread(Socket socket) throws IOException 
  36.     { 
  37.       this.socket=socket; 
  38.       is=this.socket.getInputStream(); 
  39.     } 
  40.  
  41.     @Override 
  42.    public void run() { 
  43.       try 
  44.       { 
  45.         byte[]buf=new byte[BUFFER_SIZE]; 
  46.         while(true
  47.         { 
  48.           int count=is.read(buf); 
  49.           if(count<0
  50.             break
  51.           else if(count>0
  52.           { 
  53.  
  54.           } 
  55.         } 
  56.       }catch(IOException e) 
  57.       { 
  58.  
  59.       } 
  60.    } 
  61.  
  62.     @Override 
  63.     public void cancel() { 
  64.       try { 
  65.         socket.close(); 
  66.       } catch (IOException e) { 
  67.  
  68.       } 
  69.     } 
  70.  
  71.     @Override 
  72.     public RunnableFuture<Void> newTask() { 
  73.       return new FutureTask<Void>(this,null
  74.           { 
  75.             @Override 
  76.             public boolean cancel(boolean mayInterruptIfRunning) { 
  77.               return super.cancel(mayInterruptIfRunning); 
  78.               if(ReaderThread.this instanceof CancelableRunnable)) 
  79.               { 
  80.                 ((CancelableRunnable)(ReaderThread.this)).cancel(); 
  81.               }else 
  82.               { 
  83.                 super.cancel(mayInterruptIfRunning); 
  84.               } 
  85.             } 
  86.           }; 
  87.  
  88.     } 

当你调用future的cancel的方法时,它会关闭socket,最终导致read方法异常,从而终止线程任务。


来源:51CTO

相关文章
|
4月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
381 1
|
4月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
248 1
|
4月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
254 0
|
4月前
|
存储 Java Go
【Java】(3)8种基本数据类型的分析、数据类型转换规则、转义字符的列举
牢记类型转换规则在脑海中将编译和运行两个阶段分开,这是两个不同的阶段,不要弄混!
264 2
|
4月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
266 2
|
5月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
222 0
|
5月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
411 16
|
6月前
|
缓存 并行计算 安全
关于Java多线程详解
本文深入讲解Java多线程编程,涵盖基础概念、线程创建与管理、同步机制、并发工具类、线程池、线程安全集合、实战案例及常见问题解决方案,助你掌握高性能并发编程技巧,应对多线程开发中的挑战。
|
6月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
201 4