为什么说volatile+interrupt是停止线程最优雅的姿势?

2021-03-11 10:49:02 浏览数 (2)

使用stop方法

调用stop方法,会让正在运行的线程直接中止,有可能会让一些清理性的工作得不到完成。并且stop已经被标记为废弃的方法,不建议使用。

正确的使用姿势是使用两阶段终止的模式,即一个线程发送终止指令,另一个线程接收指令,并且决定自己在何时停止。

使用标志位

代码语言:javascript复制
public class RunTask {

    private volatile boolean stopFlag;
    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (!stopFlag) {
                System.out.println("doSomething");
            }
        });
        taskThread.start();
    }

    public void stop() {
        stopFlag = true;
    }
}

「stopFlag上加volatile是保证可见性。我这个例子用了while循环不断判断,如果项目中用不到while的话,可以在关键节点判断,然后退出run方法即可」

使用interrupt方法

假如我们的任务中有阻塞的逻辑,如调用了Thread.sleep方法,如何让线程停止呢?

从线程状态转换图中寻找答案

从图中可以看到如果想让线程进入终止状态的前提是这个线程处于运行状态。当我们想要终止一个线程的时候,如果此时线程处于阻塞状态,我们如何把它转换到运行状态呢?

我们可以通过调用Thread#interrupt方法,将阻塞状态的线程转换到就绪状态,进入由操作系统调度成运行状态,即可终止。

那线程在运行状态中调用interrupt方法,会发生什么呢?

代码语言:javascript复制
public class RunTaskCase1 {

    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (true) {
                System.out.println("doSomething");
            }
        });
        taskThread.start();
    }

    public void stop() {
        taskThread.interrupt();
    }
}

依次调用start方法和stop方法,发现线程并没有停止。

「其实当线程处于运行状态时,interrupt方法只是在当前线程打了一个停止的标记,停止的逻辑需要我们自己去实现」

「Thread类提供了如下2个方法来判断线程是否是中断状态」

  1. isInterrupted
  2. interrupted

这2个方法虽然都能判断状态,但是有细微的差别

代码语言:javascript复制
 @Test
 public void testInterrupt() throws InterruptedException {
     Thread thread = new Thread(() -> {
         while (true) {}
     });
     thread.start();
     TimeUnit.MICROSECONDS.sleep(100);
     thread.interrupt();
     // true
     System.out.println(thread.isInterrupted());
     // true
     System.out.println(thread.isInterrupted());
     // true
     System.out.println(thread.isInterrupted());
 }
代码语言:javascript复制
 @Test
 public void testInterrupt2() {
     Thread.currentThread().interrupt();
     // true
     System.out.println(Thread.interrupted());
     // false
     System.out.println(Thread.interrupted());
     // false
     System.out.println(Thread.interrupted());
 }

「isInterrupted和interrupted的方法区别如下」

Thread#isInterrupted:测试线程是否是中断状态,执行后不更改状态标志 Thread#interrupted:测试线程是否是中断状态,执行后将中断标志更改为false

「所以此时我们不需要自已定义状态,直接用中断标志即可,之前的代码可以改为如下」

代码语言:javascript复制
public class RunTaskCase2 {

    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("doSomething");
            }
        });
        taskThread.start();
    }

    public void stop() {
        taskThread.interrupt();
    }
}

当线程处于阻塞状态时,调用interrupt方法,会抛出InterruptedException,也能终止线程的执行

「注意:发生异常时线程的中断标志为会由true更改为false。」

所以我们有如下实现 当线程处于运行状态:用自己定义的标志位来退出 当线程处于阻塞状态:用抛异常的方式来退出

代码语言:javascript复制
public class RunTaskCase3 {

    private volatile boolean stopFlag;
    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (stopFlag) {
                try {
                    System.out.println("doSomething");
                    TimeUnit.MICROSECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        taskThread.start();
    }

    public void stop() {
        stopFlag = true;
        taskThread.interrupt();
    }
}

当然也可以一直用中断标志来退出,「注意,当发生异常的时候需要重置中断标志位」

代码语言:javascript复制
public class RunTaskCase4 {

    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println("doSomething");
                    TimeUnit.MICROSECONDS.sleep(100);
                } catch (InterruptedException e) {
                    // 重置中断标志位为true
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        });
        taskThread.start();
    }

    public void stop() {
        taskThread.interrupt();
    }
}

最后问大家一个问题?RunTaskCase3和RunTaskCase4哪种实现方式比较好呢?

「虽然RunTaskCase4代码看起来更简洁,但是RunTaskCase4不建议使用,因为如果在run方法中调用了第三方类库,发生了InterruptedException异常,但是没有重置中断标志位,会导致线程一直运行下去,同理RunTaskCase2也不建议使用」

0 人点赞