面试题七:如何停止一个线程?

如何停止一个线程?

一、面试官视角:这道题想考察什么?

  • 是否对线程的用法有了解
  • 是否对线程的stop方法有了解
  • 是否对线程stop过程中存在的问题有认识
  • 是否熟悉interrupt中断的用法
  • 是否能解释清楚使用boolean标志位的好处
  • 是否知道interrupt底层的细节
  • 通过该题目能够转移话题到线程安全,并阐述无误

二、题目剖析:

1、void stop()void stop(Throwable obj),这两个线程停止的方法已经在Jdk1.1版本被废弃了,连同着暂停和继续的方法也被废弃了。

为什么被废弃呢?因为直接停止线程是不安全的。

场景:

Thread1运行,对内存加锁写入中,此时Thread2也想访问这块内存,但它已经拿不到内存锁了,因为Thread1在访问,那么Thread2就会处于阻塞状态,等待内存锁释放,而此时Thread1被暂停了,但它仍持有内存锁,因此Thread2仍然处于阻塞状态,那暂停之后会不会恢复呢,不知道,如果不会恢复的话,那Thread2就得一直等着,这样就会导致一些运行上的问题,万一Thread2还持有了Thread1想要的锁,那就是死锁了。

此时情况又发生了变化,Thread1被直接停止了,那它会立即释放内存锁,根本没有缓冲的时间,直接停止,直接干掉,假如原本在内存中写入数据,写到一半被停止了,而此时Thread2将会获得内存锁,开始等待CPU的时间片,当获取到时间片访问内存时,发现内存状态异常,这是因为Thread1还没有执行完,就被停止了,留下了烂摊子给Thread2,那Thread2估计也得面临crash,正是因为有这种原因,官方就把线程停止的方法给废弃了,连同着暂停和继续的方法也被废弃了。

2、如何设计可以随时被中断而取消的任务线程?

线程在设计过程中,主要是任务执行的设计,线程往往与任务是强绑定的,任务执行完了,线程也就结束了。虽然现在线程不能在执行过程中被直接停止,但任务的执行是可以停止的,所以线程的运行模式,其实是个协作的任务执行模式,我们想让线程结束,其目的是想要任务结束。

因此在设计上,我们应该在任务上添加中断方式:

  • interrupt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
      目标线程:
    class InterruptableThread extends Thread {
    @Override
    public void run() {
    try {
    sleep(5000);
    }catch(InterruptedException e) {
    System.out.println("interrupted!");
    }
    }
    }

    中断通知:
    public static void interruptAThread() {
    Thread thread = new InterruptableThread();
    thread.start();
    try {
    Thread.sleep(2000);
    }catch(InterruptedException e) {
    e.printStackTrace();
    }
    thread.interrupt();
    }
    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
      interrupt有的情况也是不支持的!

    目标线程:
    class UninterruptableThread extends Thread {
    public void run() {
    super.run();
    // 在for循环中,循环很多次的时候,是不支持interrupt的
    for(int i=0;i<100000;i++) {
    if(i%10000 == 0) {
    System.out.println(i);
    }
    }
    }
    }

    中断通知:
    public static void cannotInterruptAThread() {
    Thread thread = new UninterruptableThread();
    thread.start();
    thread.interrupt();
    }

    想要支持interrupt,可以修改一下目标线程:
    class InterruptableThread1 extends Thread {
    public void run() {
    super.run();
    for(int i=0;i<100000;i++) {
    // 每次循环都调用一下interrupt(),确认有没有中断通知,如果收到中断通知了,这个时候我们也可以中断,也可以不中断,当然按照中断模式来的话,就停止任务,此时线程也就结束了
    // 关于中断状态的读取,有两个方法:
    // 1、interrupted(),是静态方法,获取当前线程的中断状态,并清空
    // 1.1 当前运行的线程
    // 1.2 中断状态调用后清空,重复调用后续返回false【thread.cc代码中:SetInterruptedLocked(false); // 清空当前中断状态】
    //
    // 2、isInterrupted(),是非静态方法,获取该线程的中断状态,不清空
    // 2.1 调用的线程对象对应的线程
    // 2.2 可重复调用,中断清空前一直返回true
    if(interrupted()) {
    break;
    }
    if(i%10000 == 0) {
    System.out.println(i);
    }
    }
    }
    }

    中断通知:
    public static void handleInterruptionThread() {
    Thread thread = new InterruptableThread1();
    thread.start();
    thread.interrupt();
    }
  • boolean标志位

    其实interrupt代码逻辑底层也使用了boolean标志位,只是底层代码实现时还加了锁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
      目标线程:
    class InterruptableThread extends Thread {
    // boolean isStopped = false; // 此时的isStopped没有加锁,会存在线程间可见性问题,就是在中断通知那里改了true,这里未必能看得到,这是因为Java的内存模型导致的。
    // 为了保证线程间可见性,需要加一个volatile,告诉虚拟机,这个字段是易变的,易变的意思就是,哪里改了这个字段的内容,我这里就得看到。
    volatile boolean isStopped = false;
    public void run() {
    super.run();
    for(int i=0;i<100000;i++) {
    if(isStopped) {
    return;
    }
    }
    }
    }

    中断通知:
    public static void booleanFlag() {
    InterruptableThread thread = new InterruptableThread();
    thread.start();
    ...
    thread.isStopped = true;
    ...
    }
  • interrupt与boolean标志位对比

    interrupt boolean
    系统方法(sleep)
    使用JNI
    加锁
    触发方式 抛异常 布尔值判断,也可抛异常
    • 需要支持系统方法时用中断(功能性)
    • 其他情况用boolean标志位(性能)

三、题目结论:

  • 为什么线程不应该被直接stop?因为涉及到资源清理的问题,可能会给其它线程留下烂摊子。
  • 线程内置中断机制的使用与原理:通过volatile、boolean标志位通知线程停止