查看: 505|回复: 0

[Java代码] 深入分析Java线程中断机制

发表于 2017-1-7 12:00:03
Thread.interrupt真的能中断线程吗

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

但是调用此方法线程真的会停止吗?我们写个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. t.interrupt();
  12. System.out.println("is interrupt.......");
  13. }
  14. public static class NRunnable implements Runnable
  15. {
  16. @Override
  17. public void run() {
  18. while(true)
  19. {
  20. System.out.println("我没有种中断");
  21. try {
  22. Thread.sleep(1000);
  23. } catch (InterruptedException e) {
  24. }
  25. }
  26. }
  27. }
  28. }
复制代码

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

  1. is start.......
  2. 我没有种中断
  3. 我没有种中断
  4. 我没有种中断
  5. 我没有种中断
  6. 我没有种中断
  7. is interrupt.......
  8. 我没有种中断
  9. 我没有种中断
  10. 我没有种中断
  11. 我没有种中断
  12. 我没有种中断
  13. ....
复制代码

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

所以 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. run.cancel();
  11. System.out.println("cancel ..."+System.currentTimeMillis());
  12. }
  13. public static class NRunnable implements Runnable
  14. {
  15. public boolean isCancel=false;
  16. @Override
  17. public void run() {
  18. while(!isCancel)
  19. {
  20. System.out.println("我没有种中断");
  21. try {
  22. Thread.sleep(10000);
  23. } catch (InterruptedException e) {
  24. }
  25. }
  26. System.out.println("我已经结束了..."+System.currentTimeMillis());
  27. }
  28. public void cancel()
  29. {
  30. this.isCancel=true;
  31. }
  32. }
复制代码

执行结果如下:

  1. is start.......
  2. 我没有种中断
  3. cancel ...1438396915809
  4. 我已经结束了...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. System.out.println("start interrupt..."+System.currentTimeMillis());
  10. t.interrupt();
  11. System.out.println("end interrupt ..."+System.currentTimeMillis());
  12. }
  13. public static class NThread extends Thread
  14. {
  15. @Override
  16. public void run() {
  17. while(!this.isInterrupted())
  18. {
  19. System.out.println("我没有种中断");
  20. try {
  21. Thread.sleep(10000);
  22. } catch (InterruptedException e) {
  23. Thread.currentThread().interrupt();
  24. }
  25. }
  26. System.out.println("我已经结束了..."+System.currentTimeMillis());
  27. }
  28. }
  29. }
复制代码

运行结果如下:

  1. is start.......
  2. 我没有种中断
  3. start interrupt...1438398800110
  4. 我已经结束了...1438398800110
  5. 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. {
  3. public static final int BUFFER_SIZE=512;
  4. Socket socket;
  5. InputStream is;
  6. public ReaderThread(Socket socket) throws IOException
  7. {
  8. this.socket=socket;
  9. is=this.socket.getInputStream();
  10. }
  11. @Override
  12. public void interrupt() {
  13. try
  14. {
  15. socket.close();
  16. }catch(IOException e)
  17. {
  18. }finally
  19. {
  20. super.interrupt();
  21. }
  22. super.interrupt();
  23. }
  24. @Override
  25. public void run() {
  26. try
  27. {
  28. byte[]buf=new byte[BUFFER_SIZE];
  29. while(true)
  30. {
  31. int count=is.read(buf);
  32. if(count<0)
  33. break;
  34. else if(count>0)
  35. {
  36. }
  37. }
  38. }catch(IOException e)
  39. {
  40. }
  41. }
  42. }
  43. }
复制代码

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

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

改写线程池的newTaskFor方法

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

  1. Future<?> future=executor.submit(new Runnable(){
  2. @Override
  3. public void run() {
  4. }
  5. });
复制代码

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

  1. public Future<?> submit(Runnable task) {
  2. if (task == null) throw new NullPointerException();
  3. RunnableFuture<Void> ftask = newTaskFor(task, null);
  4. execute(ftask);
  5. return ftask;
  6. }
复制代码

这里调用的是AbstractExecutorService 的newTaskFor方法,那么我们能不能改写ThreadPoolExecutor的newTaskFor方法呢,接下来看我在处理吧

定义一个基类,所有需要取消的任务继承这个基类

  1. public interface CancelableRunnable<T> extends Runnable {
  2. public void cancel();
  3. public RunnableFuture<T> newTask();
  4. }
复制代码

将上面的ReaderThread改为继承这个类

  1. public static class ReaderThread implements CancelableRunnable<Void>
  2. {
  3. public static final int BUFFER_SIZE=512;
  4. Socket socket;
  5. InputStream is;
  6. public ReaderThread(Socket socket) throws IOException
  7. {
  8. this.socket=socket;
  9. is=this.socket.getInputStream();
  10. }
  11. @Override
  12. public void run() {
  13. try
  14. {
  15. byte[]buf=new byte[BUFFER_SIZE];
  16. while(true)
  17. {
  18. int count=is.read(buf);
  19. if(count<0)
  20. break;
  21. else if(count>0)
  22. {
  23. }
  24. }
  25. }catch(IOException e)
  26. {
  27. }
  28. }
  29. @Override
  30. public void cancel() {
  31. try {
  32. socket.close();
  33. } catch (IOException e) {
  34. }
  35. }
  36. @Override
  37. public RunnableFuture<Void> newTask() {
  38. return new FutureTask<Void>(this,null)
  39. {
  40. @Override
  41. public boolean cancel(boolean mayInterruptIfRunning) {
  42. return super.cancel(mayInterruptIfRunning);
  43. if(ReaderThread.this instanceof CancelableRunnable))
  44. {
  45. ((CancelableRunnable)(ReaderThread.this)).cancel();
  46. }else
  47. {
  48. super.cancel(mayInterruptIfRunning);
  49. }
  50. }
  51. };
  52. }
  53. }
复制代码

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

来源:yuanzeyao


回复

使用道具 举报