Java基础知识强化16:深入分析Java线程中断机制

1.Thread.interrupt真的能中断线程吗?

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

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

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

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

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

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

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

  • interrupt 此方法是实例方法,用于告诉此线程外部有中断请求,并且将线程中的中断标记设置为true
  • interrupted 此方法是类方法,测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。
  • isInterrupted 此方法是实例方法测试线程是否已经中断。线程的中断状态 不受该方法的影响。 线程中断被忽略,因为在中断时不处于活动状态的线程将由此返回 false 的方法反映出来

2.处理线程中断的常用方法

(1)常用方法:设置取消标记.

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

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

执行结果如下:

is start.......
我没有种中断
cancel ...1441334736290
我已经结束了...1441334743309

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

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

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

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

运行结果:

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

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

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

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

(3)处理不支持中断的线程中断的常用方法:改写线程的interrupt方法

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

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

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

(4)改写线程池的newTaskFor方法

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

Future<?> future=executor.submit(new Runnable(){
      @Override
      public void run() {

      }
    });

取消任务时,调用的是future的cancel方法,其实在cancel方法中调用的是线程的interrupt方法。所以对于不支持中断的任务cancel也是无效的,下面我们看看submit方法里面干了上面吧:

public Future<?> submit(Runnable task) {
        if (task == null) throw 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);
              }
            }
          };

    }
 }

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

时间: 2024-08-10 03:31:45

Java基础知识强化16:深入分析Java线程中断机制的相关文章

java基础知识强化52:Java程序员面试失败的5大原因

下面是Java程序员面试失败最有可能的5大原因,当然也许这5点原因适用于所有的程序员,所以,如果你是程序员,请认真阅读以下内容. 1 说得太少 尤其是那些开放式的问题,如“请介绍下你自己”或“请讲一下你曾经解决过的复杂问题”.面试官会通过你对这些技术和非技术问题的回答来评估你的激情.他们也会通过模拟团队氛围和与你的交流互动来判断你的经验和能力. 所以,仅仅只用两三句话来回答不但不能显示出你对这个专业的兴趣,还会让整个面试过程显得非常无聊.如果你不能很好地说明你的经验.成就和技能可以给企业带来的价

Java基础知识强化99:Java 常见异常及趣味解释

常见 Java 异常解释:(译者注:非技术角度分析.阅读有风险,理解需谨慎:) 1. java.langjava.lang软件包是java语言的核心部分,它提供了java中的基础类. java.lang.Object,这是java.lang的根类,也是所有java类的超类. java.lang ArithmeticException 出现异常的运算条件时,抛出此异常.例如,一个整数"除以零" 你正在试图使用电脑解决一个自己解决不了的数学问题,请重新阅读你的算术表达式并再次尝试. Arr

Java基础知识强化10:Java中的中间缓存变量机制

1.对于自增运算++j与j++,由于加一的执行顺序不同,所以Java中有中间缓存变量来储存其单个表达式的值,而j的自增自减的结果依然保留在原来的变量储存区.因为本体是j的值,而单个表达式的值是中间产生的一个临时变量值,是在整条计算表达式结束后就可以抛弃的值,所以用个临时中间缓存变量在放就可以了.这就可以实现自增自减运算在计算时值的加减1顺序差异产生的表达式与本体值差异的两个变量储存. 2. 1 因为在计算过程中,使用了Java中间变量缓存机制.在java中,执行自增运算时,会为每一个自增操作分配

Java基础知识强化101:Java 中的 String对象真的不可变吗 ?

1. 什么是不可变对象?       众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的. 不能改变状态的意思是:不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变. 2. 区分对象和对象的引用 对于Java初学者, 对于String是不可变对象总是存有疑惑.看下面代码: 1 String s =

Java基础知识强化104:Java常量池理解与总结

一.相关概念 1. 什么是常量 用final修饰的成员变量表示常量,值一旦给定就无法改变! final修饰的变量有三种:静态变量.实例变量和局部变量,分别表示三种类型的常量. 2. Class文件中的常量池 在Class文件结构中,最头的4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受:再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号:再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(const

Java基础知识强化106:Java中如何实现十进制到其他进制的转换

下面是示例代码,我们直接通过JDK工具库中的方法实现的,如下: 1 package com.himi.radix; 2 3 4 /** 5 * Java中如何实现十进制到其他进制的转换 6 * @author hebao 7 * 8 */ 9 public class IntegerToOther { 10 11 public static void main(String[] args) { 12 int n = 14; 13 //十进制转成十六进制: 14 String n0 = Integ

Java基础知识强化13:Java中单例模式案例使用

1.古往今来历史上皇帝通常只有一人.为了保证其唯一性,古人采用增加"防伪标识"的办法,如玉玺.更为简单的办法就是限制皇帝的创建.本案例中就是使用单例模式从而保证皇帝的唯一性.实例运行如下:代码: (1)Empreror.java:*(定义的皇帝类) 1 package himi.only; 2 3 public class Empreror { 4 private static Empreror empreror = null; 5 private Empreror() { 6 7 }

Java基础知识强化03:Java中的堆与栈

1.在JVM中,内存分为两个部分,Stack(栈)和Heap(堆),这里,我们从JVM的内存管理原理的角度来认识Stack和Heap,并通过这些原理认清Java中静态方法和静态属性的问题. 一般,JVM的内存分为两部分:Stack和Heap.  注意: java程序运行时,数据会分区存放,heap.stack.method. 类的对象放在heap(堆)中,所有的类对象都是通过new方法创建,创建后,在stack(栈)会创建类对象的引用(内存地址). stack的区域很小,只有1M,特点是存取速度

Java基础知识强化12:Java中运用数组的四种排序方法

1.使用JavaApi文档中的Arrays类中的sort()进行快速排序 首先我们直接看代码如下: 1 package himi.text; 2 3 import java.util.Arrays; 4 5 public class TestDemo01 { 6 7 public static void main(String[] args) { 8 int[] array = {2,12,3,44,27}; 9 /** 10 * 利用使用JavaApi文档中的Arrays类中的sort()进行