前言
线程间的交互方式一般有:
- 一个线程启动另一个线程。
- 一个线程终结另一个线程。
- 多个线程相互配合完成任务。
第一种我们已经比较熟悉了,来看第二种和第三种。
stop() 和 interrupt()
java
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
System.out.println("i = " + i);
}
}
};
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.stop();
之前,我们可以使用 Thread.stop()
来停止线程,但它已经被弃用了。甚至方法实现都被注释掉了,始终会抛出 UnsupportedOperationException
异常。
java
@Deprecated(since="1.2")
public final void stop() {
/*
... 源码实现 ...
*/
throw new UnsupportedOperationException();
}
为什么呢?
并不是因为它无效而被弃用,而是因为它突然终止线程,不管线程当前在做什么,都会被强制停止,这会导致数据不一致问题。
比如一个任务只执行到了一半时就被终止,会让持有的锁无法被释放,非常危险。
解决办法是使用中断(Interrupt)机制,即使用 Thread.interrupt()
,像这样:
java
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
System.out.println("i = " + i);
}
}
};
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.interrupt();
运行会发现,子线程还是从 0 打印到了 999999,难道这个方法没起作用吗?
其实 interrupt()
只会将线程标记为中断状态,它是一个温和的通知,告诉线程:"我希望你结束"。但是否结束,是由线程内部决定的。
我们就来支持一下:
java
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000; i++) {
// 检查中断状态
if (isInterrupted()) {
// 线程被中断,执行收尾工作
// ...
System.out.println("thread is interrupted");
return; // 响应中断,直接退出
}
System.out.println("i = " + i);
}
}
};
再次运行就能看到:
less
...
i = 829359
i = 829360
thread is interrupted
其中 isInterrupted()
会返回是否被打断的标记,如果被其他线程中断了,就会返回 true。我们通常会在各种耗时操作的前面进行判断,以免无意义的资源浪费,因为都要被终止了。
Thread.interrupted()
也能够进行判断,与 isInterrupted()
不同的是,它内部会将标记置为 false
。
补充:为什么 Thread.sleep()
需要捕获 InterruptedException
异常?
因为处于等待(Waiting)状态(如 sleep
, wait
, join
)的线程也是可以被终止的。
子线程在睡眠时被打断,并不会导致状态不一致。因此,为了让线程能够立刻中断,JVM 会通过抛出 InterruptedException
异常的方式来立即唤醒子线程,我们可以在 catch
分支中进行收尾工作。
另外,当抛出 InterruptedException
时,会清除线程中断的标志位,即将标记置为 false
。如果我们想要在后续代码中,依然能够检测到中断,需要在 catch
分支中将中断状态设为 true
(Thread.currentThread().interrupt()
)。
java
Thread thread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// ... 执行收尾工作 ...
System.out.println("interrupted");
// 此时中断标志位已被清除为 false
// 如果希望后续代码继续响应中断,需要再次设置
Thread.currentThread().interrupt();
}
}
};
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
thread.interrupt();
wait() 和 notifyAll()
我们再来看看第三种:两个线程的配合操作。来看一个例子:
java
public class WaitDemo {
private String data;
public synchronized void initData() {
this.data = "data";
}
public synchronized void printData() {
System.out.println("data is " + data);
}
public void run() {
Thread threadA = new Thread() { // 打印线程
@Override
public void run() {
try {
Thread.sleep(1000); // 1秒后打印
} catch (InterruptedException ignored) {
}
printData();
}
};
Thread threadB = new Thread() { // 初始化线程
@Override
public void run() {
try {
Thread.sleep(2000); // 2秒后初始化
} catch (InterruptedException ignored) {
}
initData();
}
};
threadB.start();
threadA.start();
}
}
现在运行,结果会是 data is null
,因为 threadA
打印时,threadB
还未初始化 data
。
往往一个线程的工作,需要另一个线程的执行结果,这时我们可以在 printData
中不断检查 data
的值,直至不为空。
java
public synchronized void printData() {
while (data == null){
// 空转,等待 data 被赋值
}
System.out.println("data is " + data);
}
这样有个问题:printData()
方法取得了锁,它会导致其他线程无法进入 initData()
来修改 data
的值,造成死锁。
这个时候需要使用 wait()
方法,它会让当前线程释放持有的锁,并让当前线程进入等待池中(不是锁池),使自己处于等待状态,不占用 CPU。
java
public synchronized void printData() {
// 这里必须使用 while 循环
while (data == null) {
try {
wait(); // 释放锁,进入等待池
} catch (InterruptedException e) {
// 执行收尾工作
System.out.println("thread is interrupted");
// 中断标志位同样会被清除
Thread.currentThread().interrupt();
return;
}
}
System.out.println("data is " + data);
}
线程可能会在未被 notify
的情况下醒来,此时 data
仍为 null
,使用 while 能确保只有在满足条件的情况下,才能执行后续代码。
怎么通知等待线程呢?只需调用 notify
或 notifyAll
方法即可。它会让处于等待池中的线程被唤醒,转变为可运行状态,再重新进入锁池去竞争取得锁,竞争到锁的线程会从 wait()
方法处返回,继续执行。
java
public synchronized void initData() {
this.data = "data";
notifyAll(); // 唤醒所有在等待池中的线程
}
不过,notify
只会唤醒一个等待的线程,我们更多会调用 notifyAll()
,来唤醒所有的等待线程。
现在运行的结果将会是 data is data
。
注意:wait
和 notifyAll
必须配合使用,否则会导致线程进入无休止的等待。
另外,wait
和 notifyAll
都是 Object 类中的方法,并不是 Thread 类中的方法。因为唤醒线程和让线程等待的主体并不是 Thread,而是当前同步块中的监视器(Monitor),也就是锁对象。
比如我们可以这样写,使用一个 Object 对象作为锁:
java
private final Object monitor = new Object();
public void initData() {
synchronized (monitor) {
this.data = "data";
monitor.notifyAll(); // 唤醒所有等待 monitor 锁的线程
}
}
public void printData() {
synchronized (monitor) {
while (data == null) {
try {
monitor.wait(); // 在 monitor 对象上等待
} catch (InterruptedException e) {
// 执行收尾工作
System.out.println("thread is interrupted");
Thread.currentThread().interrupt();
return;
}
}
System.out.println("data is " + data);
}
}
join()
Thread.join()
方法用于将指定线程放在当前线程之前执行,当前线程将被挂起(进入等待状态),直至指定线程执行完毕。
它本质上也是一种等待,所以调用 join()
的线程如果在等待期间被中断,也会抛出 InterruptedException
异常。
例如,这样也能正常工作:
java
private String data;
public synchronized void initData() {
this.data = "data";
}
public synchronized void printData() {
System.out.println("data is " + data);
}
public void run() {
Thread threadB = new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException ignored) {
}
initData();
}
};
Thread threadA = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
try {
// 等待 threadB 执行完毕
threadB.join();
} catch (InterruptedException e) {
// 如果 threadA 在等待时被中断
System.out.println("threadA waiting for B was interrupted");
Thread.currentThread().interrupt(); // 重新设置中断状态
return;
}
printData();
}
};
threadB.start();
threadA.start();
}
不过 wait
和 notify
的方式更灵活,还是更加常用的。
yield()
Thread.yield()
在线程内部调用,它用于提示线程调度器将 CPU 时间片让给同优先级的其他线程。
yield()
之后的线程会立即回到就绪状态,而不是等待状态。