但你下载一个大的文件时,可能由于网速过慢,导致用户想取消下载。这时就需要中断下载线程的执行。
这里的中断是指将当前的线程中断,中断不代表结束当前线程。
可以调用 interrupt()
来在其他线程中对目标线程进行中断请求:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停当前线程 1 毫秒, 此时会在 MyThread 线程中输出不等量的 (``n+" hello!")
t.interrupt(); // 中断 t 线程
t.join(); // 等待 t 线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
}
}
}
main
线程通过调用 t.interrupt()
方法中断 t
线程,但是要注意,interrupt()
方法仅仅向 t
线程发出了「中断请求」,至于 t
线程是否能立刻响应,要看具体代码。而 t
线程的 while
循环会检测 isInterrupted()
,所以上述代码能正确响应 interrupt()
请求,使得自身立刻结束运行 run()
方法。
如果线程处于等待状态,例如,t.join()
会让 main
线程进入等待状态,此时,如果对 main
线程调用 interrupt()
,join()
方法会立刻抛出 InterruptedException
,因此,目标线程只要捕获到 join()
方法抛出的 InterruptedException
,就说明有其他线程对其调用了 interrupt()
方法,通常情况下该线程应该立刻结束运行。
个人的理解为:不能对等待的线程调用中断请求。
例如下面例子:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断 t 线程。因为 t 线程中调用 hello.join() 进入等待状态,此时中断 t 线程会导致 t 线程内部抛出异常
t.join(); // 等待 t 线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}
class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
main
线程通过调用 t.interrupt()
从而通知 t
线程中断,而此时 t
线程正位于 hello.join()
的等待中,此方法会立刻结束等待并抛出 InterruptedException
。由于在 t
线程中捕获了 InterruptedException
,因此,就可以准备结束该线程。在 t
线程结束前,对 hello
线程也进行了 interrupt()
调用通知其中断。如果去掉这一行代码,可以发现 hello
线程仍然会继续运行,且 JVM 不会退出。
另一个常用的中断线程的方法是设置标志位。我们通常会用一个 running
标志位来标识线程是否应该继续运行,在外部线程中,通过把 HelloThread.running
置为 false
,就可以让线程结束:
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为 false 来中断线程
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
HelloThread
的标志位 boolean running
是一个线程间共享的变量。线程间共享变量需要使用 volatile
关键字标记,确保每个线程都能读取到更新后的变量值。
在 Java 虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Main Memory
│ │
┌───────┐┌───────┐┌───────┐
│ │ var A ││ var B ││ var C │ │
└───────┘└───────┘└───────┘
│ │ ▲ │ ▲ │
─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─
│ │ │ │
┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐
▼ │ ▼ │
│ ┌───────┐ │ │ ┌───────┐ │
│ var A │ │ var C │
│ └───────┘ │ │ └───────┘ │
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
关键字解决了共享变量在线程间的可见性问题。