线程、Thread类和线程终止相关整理(中)——线程终止(Cancellation)

“将在外,君命有所不受。”这句来自《三国演义》,最早源于《孙子兵法》的“城有所不攻,地有所不争,君命有所不受”。大概意思是,指挥战争的将士在战场前线,可以随机应变,根据战场的实时情况进行合理决策,而没必要向君主汇报战况请求决定。以这句话开始说Java线程的中止可能也不完全恰当,但仔细想想确有类似之处。这篇文章内容主要说说线程的终止(又叫任务取消)。

0. 我们先考虑一个最简单的线程终止的情况。当我们运行main()方法的时候,实例化了一个Thread对象,并start(),这时一个新线程诞生并开始执行其run()方法了。当run()方法中不存在死循环,那么run()方法执行完毕,新线程任务完成,圆满结束了。

1. 稍微复杂一点的情况,新线程循环执行,需要配合主线程的指令才肯结束。我们可以考虑一个boolean类型的变量,新线程每次循环开始时检查这个变量,不符合终止条件则跑一圈任务,回来在检查,主线程只要能设置这个变量到合适条件,新线程跑完某一圈的时候就break,最后run()结束。

在这里面有一个需要注意的地方,就是主线程仅仅是设置一个boolean的标记,设置之后不能保证新线程立即结束,而是要跑完一圈回来才结束,那么这个检查标记的过程就是一个检查点,程序以“一圈”为最小运行单位,跑完特定的某圈才会到检查点,到检查点才有可能终止任务。如果这“一圈”的任务较长,则可能会有时间上明显的延迟,从这点看,这种终止实际上不是绝对实时的。

其实,本文开头说那段话的原因就在于此。是在Java中没有一个绝对的机制保证执行中的线程能够立刻响应请求安全完整地终止,而更多情况下是即使向线程发送了终止请求,考虑到状态的一致性,线程还是会继续执行,直到线程任务本身认为时机适合执行停止操作。至于实时,这本身就是一个相对的概念,想想身边的自然,就算是最快的光,从太阳到地球还需要8分钟呢……

2. 除了“实时”终止的问题,还有一点就是程序“跑累”了要歇一会儿,会有前文提到的BLOCKED状态。不管是那种原因造成的BLOCKED状态,代码不往下走了,检查点自然也就不会被检查,不会被检查run()就不会执行到最后,新线程也就没办法结束。

为了解决这个问题,我们先看下造成线程BLOCK住的几个可能的因素。

  • sleep()、wait()、join()等方法的调用
  • IO阻塞,比如一个System.in在read(),但却没有直接数据进来
  • synchronized锁等待,多个线程竞争锁,某个线程没竞争到,在等

在《Java Concurrency in Practice》书中,对IO阻塞做了更详细的情况划分(同步BIO阻塞、同步NIO阻塞和selector的NIO阻塞),这里就先简要这么分为一类了。

先重点说下第一种情况,也是本篇文章的重点,解决方案就是中断(interrupt)。下面是Thread的sleep()方法。

public static native void sleep(long millis) throws InterruptedException;

静态、native实现,更重要的,会throws InterruptedException。就是说在写代码的时候,这个方法的调用要么套个try-catch块,要么调用所在的方法要继续throws InterruptedException,否则编译器都不让你过这一关。那么什么时候抛这个异常呢?中断异常自然是中断的时候了——Thread的另一个方法,不过是非静态的,是实例方法:

public void interrupt(){ … }

咦!这个方法不是native本地实现!不要着急,它的源码中最终调用了这个方法……

private native void interrupt0();

下面说说中断使用。interrupt()所做的实际上和上面描述的“检查点”方案类似,就是给被调用的线程发了个新号打了个interrupt标记。和这个标记相关还有几个方法和刚刚那个异常

  • public boolean isInterrupted()
  • public static boolean interrupted()
  • InterruptedException

两个方法有看起来都是判断是否发生异常的,但有两个区别,第一个方法(暂作A)是非静态的实例方法,需要有对象引用,而第二个(B)则是静态方法,是对Thread类的调用。

再瞅一眼这两个方法在Sun JDK中源码,他们都调用了同一个另外的方法:

private native boolean isInterrupted(boolean ClearInterrupted);

又一个native的方法,在A方法中,传入参数ClearInterrupted为false,而在B方法中参数则为true。这个参数是啥意思呢,懂英文单词估计就可以猜到,是决定是否清楚这个中断信号标记的。就是说调用A方法,不影响后面再调用判断,而调用interrupted()方法,需要自己用一个boolean变量记录返回结果,后面再次调用就已经是清理标记位的了。

关于使用中断的情况,还有几点要强调的:

  • “中断”的概念实际上并不能保证实际的中断,而只是一个信号发过去,给目标线程打了一个标记(谈到这点,貌似和Linux下的kill有所类似),至于要不要中断怎么中断是实现来决定的
  • 对于sleep()、wait()等方法,会抛出异常来做到中断,需要catch的时候做中断处理,不处理就只是抛出个异常,如果新线程有循环,那么任务还是会继续
  • 还是针对sleep()等方法,抛出异常的同时,实际上也就清空了中断标记,再去调用刚说那几个方法(isInterrupt()和interrupted())没什么意义
  • 对于非sleep()方法或者没有阻塞的线程,一个interrupt()方法调用过去,则情况和1中说的一样,只是一个标记的作用,在“检查点”可以用isInterrupt()和interrupted()来判断并处理
  • 和前面1中的“检查点”的一样,就算是对中断做出处理,也不是保证“实时”

3. 第2点说了好长,情况有点多,但主要还是中断能解决的情况,这里说下中断搞不定的情况,即IO阻塞和synchronized的锁等待。

对于IO阻塞,要想及时终止,原则很简单,就是没收IO资源,对其进行关闭处理

对于synchronized锁等待,没什么好办法……解决方案就是JavaSE5的Lock锁,有lockInterruptibly()方法,支持中断。

4. 讲Java多线程并发的第一篇文章就说了,现在对于原生的Thread对象直接操作的情况并不多,而更多是使用Executor执行器,对于使用执行器的情况,我们用Future类来操控线程终止。

ExecutorService接口(一个包含生命周期和submit方法的Executor)除了有execute(Runnable)的方法,还提供了几个重载实现的submit()方法,特点就是返回了一个Future对象。而对于Future对象简要介绍就是,可以得到线程任务返回结果,并且可以控制执行器(线程池)中的线程,使其终止。Future有一个的方法

boolean cancel(boolean mayInterruptIfRunning);

这个方法分了几种线程状态情况:

  • 如果这个对象对应的任务已经结束,则cancel直接返回false
  • 如果线程还没开始,返回true,而且线程没有机会再去执行了
  • 如果线程已经开始,boolean传入参数为false,那就不中断了
  • 如果线程已经开始,boolean传入参数为true,那就中断一下子

5. 执行器(线程池)的shutdown()和shutdownNow()方法是对执行器(线程池)中的线程进行中断。其中,前一个方法是对空闲线程进行中断通知,而后者则是对所有线程中断。

6. 对于守护线程(thread.isDaemon()返回true),则在非守护线程都终止后自行终止。

7. 对于Thread的stop()方法,已经是@Deprecated不推荐使用的了。虽然在很多Java课本中还会介绍,但容易出现状态不一致等相关问题,在实际应用场景不要轻易用。类似的还有suspend()和resume()方法等。

回头一看,文章还是好长……就说这些,不足的欢迎读者补充,谢谢!

此条目发表在 Java, Java语言, 并发, 开发, 计算机技术 分类目录,贴了 , , , , , 标签。将固定链接加入收藏夹。

线程、Thread类和线程终止相关整理(中)——线程终止(Cancellation)》有 1 条评论

  1. Pingback 引用通告: Java并发文章列表整理(上) - 程序员 - 开发者第2289322个问答

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>