但你下载一个大的文件时,可能由于网速过慢,导致用户想取消下载。这时就需要中断下载线程的执行。

这里的中断是指将当前的线程中断,中断不代表结束当前线程。

可以调用 interrupt() 来在其他线程中对目标线程进行中断请求:

  1. public class Main {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t = new MyThread();
  4. t.start();
  5. Thread.sleep(1); // 暂停当前线程 1 毫秒, 此时会在 MyThread 线程中输出不等量的 (``n+" hello!")
  6. t.interrupt(); // 中断 t 线程
  7. t.join(); // 等待 t 线程结束
  8. System.out.println("end");
  9. }
  10. }
  11. class MyThread extends Thread {
  12. public void run() {
  13. int n = 0;
  14. while (!isInterrupted()) {
  15. n++;
  16. System.out.println(n + " hello!");
  17. }
  18. }
  19. }

main 线程通过调用 t.interrupt() 方法中断 t 线程,但是要注意,interrupt() 方法仅仅t 线程发出了「中断请求」,至于 t 线程是否能立刻响应,要看具体代码。而 t 线程的 while 循环会检测 isInterrupted(),所以上述代码能正确响应 interrupt() 请求,使得自身立刻结束运行 run() 方法。
如果线程处于等待状态,例如,t.join() 会让 main 线程进入等待状态,此时,如果对 main 线程调用 interrupt()join() 方法会立刻抛出 InterruptedException,因此,目标线程只要捕获到 join() 方法抛出的 InterruptedException,就说明有其他线程对其调用了 interrupt() 方法,通常情况下该线程应该立刻结束运行。

个人的理解为:不能对等待的线程调用中断请求。

例如下面例子:

  1. public class Main {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t = new MyThread();
  4. t.start();
  5. Thread.sleep(1000);
  6. t.interrupt(); // 中断 t 线程。因为 t 线程中调用 hello.join() 进入等待状态,此时中断 t 线程会导致 t 线程内部抛出异常
  7. t.join(); // 等待 t 线程结束
  8. System.out.println("end");
  9. }
  10. }
  11. class MyThread extends Thread {
  12. public void run() {
  13. Thread hello = new HelloThread();
  14. hello.start(); // 启动hello线程
  15. try {
  16. hello.join(); // 等待hello线程结束
  17. } catch (InterruptedException e) {
  18. System.out.println("interrupted!");
  19. }
  20. hello.interrupt();
  21. }
  22. }
  23. class HelloThread extends Thread {
  24. public void run() {
  25. int n = 0;
  26. while (!isInterrupted()) {
  27. n++;
  28. System.out.println(n + " hello!");
  29. try {
  30. Thread.sleep(100);
  31. } catch (InterruptedException e) {
  32. break;
  33. }
  34. }
  35. }
  36. }

main 线程通过调用 t.interrupt() 从而通知 t 线程中断,而此时 t 线程正位于 hello.join() 的等待中,此方法会立刻结束等待并抛出 InterruptedException。由于在 t 线程中捕获了 InterruptedException,因此,就可以准备结束该线程。在 t 线程结束前,对 hello 线程也进行了 interrupt() 调用通知其中断。如果去掉这一行代码,可以发现 hello 线程仍然会继续运行,且 JVM 不会退出。
另一个常用的中断线程的方法是设置标志位。我们通常会用一个 running 标志位来标识线程是否应该继续运行,在外部线程中,通过把 HelloThread.running 置为 false,就可以让线程结束:

  1. public class Main {
  2. public static void main(String[] args) throws InterruptedException {
  3. HelloThread t = new HelloThread();
  4. t.start();
  5. Thread.sleep(1);
  6. t.running = false; // 标志位置为 false 来中断线程
  7. }
  8. }
  9. class HelloThread extends Thread {
  10. public volatile boolean running = true;
  11. public void run() {
  12. int n = 0;
  13. while (running) {
  14. n++;
  15. System.out.println(n + " hello!");
  16. }
  17. System.out.println("end!");
  18. }
  19. }

HelloThread 的标志位 boolean running 是一个线程间共享的变量。线程间共享变量需要使用 volatile 关键字标记,确保每个线程都能读取到更新后的变量值。
在 Java 虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

  1. Main Memory
  2. ┌───────┐┌───────┐┌───────┐
  3. var A ││ var B ││ var C
  4. └───────┘└───────┘└───────┘
  5. ─│─│─ ─│─│─
  6. ┌───────┐ ┌───────┐
  7. var A var C
  8. └───────┘ └───────┘
  9. Thread 1 Thread 2

这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量 a = true,线程 1 执行 a = false 时,它在此刻仅仅是把变量 a副本变成了 false,主内存的变量 a 还是 true,在 JVM 把修改后的 a 回写到主内存之前,其他线程读取到的 a 的值仍然是 true,这就造成了多线程之间共享的变量不一致。
因此,volatile 关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

可见 volatile 是保证多线程的时效性

volatile 关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
如果我们去掉 volatile 关键字,运行上述程序,发现效果和带 volatile 差不多,这是因为在 x86 的架构下,JVM 回写主内存的速度非常快,但是,换成 ARM 的架构,就会有显著的延迟。

小结

对目标线程调用 interrupt() 方法可以请求中断一个线程,目标线程通过检测 isInterrupted() 标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到 InterruptedException
目标线程检测到 isInterrupted()true 或者捕获了 InterruptedException 都应该立刻结束自身线程;
通过标志位判断需要正确使用 volatile 关键字;
volatile 关键字解决了共享变量在线程间的可见性问题。