深入分析Java线程中断机制

开发 后端
在平时的开发过程中,相信都会使用到多线程,在使用多线程时,大家也会遇到各种各样的问题,今天我们就来说说一个多线程的问题——线程中断。在 java中启动线程非常容易,大多数情况下我是让一个线程执行完自己的任务然后自己停掉,但是有时候我们需要取消某个操作,比如你在网络下载时,有时候需 要取消下载。

Thread.interrupt真的能中断线程吗

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

深入分析Java线程中断机制

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

 

public class Main { 
  private static final String TAG = "Main"
  public static void main(String[] args) { 
    Thread t=new Thread(new NRunnable()); 
    t.start(); 
    System.out.println("is start......."); 
    try { 
      Thread.sleep(3000); 
    } catch (InterruptedException e) { 
 
    } 
 
    t.interrupt(); 
    System.out.println("is interrupt......."); 
 
  } 
 
  public static class NRunnable implements Runnable 
  { 
 
    @Override 
    public void run() { 
      while(true
      { 
        System.out.println("我没有种中断"); 
        try { 
          Thread.sleep(1000); 
        } catch (InterruptedException e) { 
 
        } 
      } 
    } 
 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

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

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

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

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

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

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

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

处理线程中断的常用方法

设置取消标记

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

 

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

执行结果如下:

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

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

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

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

 

public static void main(String[] args) { 
    Thread t=new NThread(); 
    t.start(); 
    System.out.println("is start......."); 
    try { 
      Thread.sleep(3000); 
    } catch (InterruptedException e) { 
 
    } 
    System.out.println("start interrupt..."+System.currentTimeMillis()); 
    t.interrupt(); 
    System.out.println("end interrupt ..."+System.currentTimeMillis()); 
  } 
 
  public static class NThread extends Thread 
  { 
 
    @Override 
    public void run() { 
      while(!this.isInterrupted()) 
      { 
        System.out.println("我没有种中断"); 
        try { 
          Thread.sleep(10000); 
        } catch (InterruptedException e) { 
          Thread.currentThread().interrupt(); 
        } 
      } 
      System.out.println("我已经结束了..."+System.currentTimeMillis()); 
    } 
 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

运行结果如下:

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方法

 

public static class ReaderThread extends Thread 

   public static final int BUFFER_SIZE=512
   Socket socket; 
   InputStream is; 
 
   public ReaderThread(Socket socket) throws IOException 
   { 
     this.socket=socket; 
     is=this.socket.getInputStream(); 
   } 
 
   @Override 
  public void interrupt() { 
     try 
     { 
       socket.close(); 
     }catch(IOException e) 
     { 
 
     }finally 
     { 
       super.interrupt(); 
     } 
    super.interrupt(); 
  } 
   @Override 
  public void run() { 
     try 
     { 
       byte[]buf=new byte[BUFFER_SIZE]; 
       while(true
       { 
         int count=is.read(buf); 
         if(count<0
           break
         else if(count>0
         { 
 
         } 
       } 
     }catch(IOException e) 
     { 
 
     } 
  } 


  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.

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

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

改写线程池的newTaskFor方法

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

 

Future<?> future=executor.submit(new Runnable(){ 
      @Override 
      public void run() { 
 
      } 
    }); 
 
取消任务时,调用的是future的cancel方法,其实在cancel方法中调用的是线程的interrupt方法。所以对于不支持中断的任务cancel也是无效的,下面我们看看submit方法里面干了上面吧 
 
    public Future<?> submit(Runnable task) { 
        if (task == nullthrow new NullPointerException(); 
        RunnableFuture<Void> ftask = newTaskFor(task, null); 
        execute(ftask); 
        return ftask; 
    } 
 
这里调用的是AbstractExecutorService 的newTaskFor方法,那么我们能不能改写ThreadPoolExecutor的newTaskFor方法呢,接下来看我在处理吧 
 
定义一个基类,所有需要取消的任务继承这个基类 
 
public interface CancelableRunnable<T> extends Runnable { 
 
  public void cancel(); 
  public RunnableFuture<T> newTask(); 
 

 
将上面的ReaderThread改为继承这个类 
 
 public static class ReaderThread implements CancelableRunnable<Void> 
  { 
    public static final int BUFFER_SIZE=512
    Socket socket; 
    InputStream is; 
 
    public ReaderThread(Socket socket) throws IOException 
    { 
      this.socket=socket; 
      is=this.socket.getInputStream(); 
    } 
 
    @Override 
   public void run() { 
      try 
      { 
        byte[]buf=new byte[BUFFER_SIZE]; 
        while(true
        { 
          int count=is.read(buf); 
          if(count<0
            break
          else if(count>0
          { 
 
          } 
        } 
      }catch(IOException e) 
      { 
 
      } 
   } 
 
    @Override 
    public void cancel() { 
      try { 
        socket.close(); 
      } catch (IOException e) { 
 
      } 
    } 
 
    @Override 
    public RunnableFuture<Void> newTask() { 
      return new FutureTask<Void>(this,null
          { 
            @Override 
            public boolean cancel(boolean mayInterruptIfRunning) { 
              return super.cancel(mayInterruptIfRunning); 
              if(ReaderThread.this instanceof CancelableRunnable)) 
              { 
                ((CancelableRunnable)(ReaderThread.this)).cancel(); 
              }else 
              { 
                super.cancel(mayInterruptIfRunning); 
              } 
            } 
          }; 
 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.

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

责任编辑:王雪燕 来源: yuanzeyao
相关推荐

2020-12-07 06:23:48

Java内存

2010-09-07 14:21:22

PPPoE协议

2022-04-12 08:30:45

TomcatWeb 应用Servlet

2011-03-23 11:01:55

LAMP 架构

2011-09-01 13:51:52

JavaScript

2010-03-08 14:53:48

Linux分区

2023-02-01 08:13:30

Redis内存碎片

2022-08-30 07:00:18

执行引擎Hotspot虚拟机

2009-06-10 18:12:38

Equinox动态化OSGi动态化

2009-12-14 14:50:46

Ruby传参数

2009-12-16 16:39:01

Visual Stud

2021-10-29 16:36:53

AMSAndroidActivityMan

2017-02-20 10:06:12

Win32k系统调用漏洞

2011-09-13 09:08:22

架构

2018-12-18 10:11:37

软件复杂度软件系统软件开发

2023-08-07 07:44:44

2018-10-25 15:24:10

ThreadLocal内存泄漏Java

2021-04-13 12:55:06

SpringMVC解析器接口

2013-11-14 17:02:41

Android多窗口

2011-06-28 14:11:33

JavaScript
点赞
收藏

51CTO技术栈公众号