意外的线程唤醒
Unexpected thread wakeup
我原以为下例中的第二个线程会挂起,因为它等待一个没有相应通知的对象。相反,它落入了 println,大概是由于虚假唤醒。
public class Spurious {
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
System.out.println("Hey!");
}
};
Thread t2 = new Thread() {
public void run()
{
try {
synchronized (t1) {
t1.wait();
}
} catch (InterruptedException e) {
return;
}
System.out.println("Done.");
}
};
t1.start();
t2.start();
}
}
输出:
Hey!
Done.
另一方面,如果从第一个线程中删除 "Hey!" println,第二个线程确实会挂起。这在 MacOS 和 Linux.
上都会发生
知道为什么吗?
您正在等待 Thread 对象。这是一种不好的做法,在 Thread 的 javadoc 中明确不鼓励这样做(Thread.join,准确地说)。
原因是当您调用 thread.join()
阻塞直到线程停止 运行 时,您实际上是在等待线程。当线程停止 运行 时,它会发出通知,以便解除对 join()
的所有调用者的阻塞。
由于您在线程上等待,当线程停止时您会隐式收到通知 运行。
这不是虚假唤醒,虚假唤醒是由 JVM 中的竞争条件引起的。这是您代码中的竞争条件。
println 使线程 1 保持活动状态的时间刚好足以使线程 2 可以在线程 1 终止之前开始等待。
线程 1 终止后,它会向等待其监视器的所有对象发送通知。 thread2 收到通知并停止等待。
删除 println 减少了线程 1 完成所需的时间,这样线程 1 已经完成,线程 2 可以开始等待它。线程 1 不再存在,并且在线程 2 开始等待之前它的通知已经发生,因此线程 2 永远等待。
中记录了线程在死亡时发送通知
This implementation uses a loop of this.wait calls conditioned on this.isAlive. As a thread terminates the this.notifyAll method is invoked. It is recommended that applications not use wait, notify, or notifyAll on Thread instances.
(调用notifyAll的线程必须持有锁,如果其他线程抢到了锁,它可以保持终止线程存活并延迟notifyAll,直到终止线程可以获取锁。)
道德(好吧,道德之一)是始终在带有条件变量的循环中等待,参见 the Oracle tutorial。如果将 Thread2 更改为如下所示:
Thread t2 = new Thread() {
public void run()
{
try {
synchronized (t1) {
while (t1.isAlive()) {
t1.wait();
}
}
} catch (InterruptedException e) {
return;
}
System.out.println("Done.");
}
};
那么无论线程 2 是否可以在线程 1 完成之前开始等待,线程 2 都应该退出。
当然这是玩具示例的总范围:
不要扩展 Thread,使用 Runnable 或 Callable。
不要锁定线程。
不要启动线程,使用执行器。
比wait/notify更喜欢更高级别的并发结构。
我原以为下例中的第二个线程会挂起,因为它等待一个没有相应通知的对象。相反,它落入了 println,大概是由于虚假唤醒。
public class Spurious {
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
System.out.println("Hey!");
}
};
Thread t2 = new Thread() {
public void run()
{
try {
synchronized (t1) {
t1.wait();
}
} catch (InterruptedException e) {
return;
}
System.out.println("Done.");
}
};
t1.start();
t2.start();
}
}
输出:
Hey!
Done.
另一方面,如果从第一个线程中删除 "Hey!" println,第二个线程确实会挂起。这在 MacOS 和 Linux.
上都会发生知道为什么吗?
您正在等待 Thread 对象。这是一种不好的做法,在 Thread 的 javadoc 中明确不鼓励这样做(Thread.join,准确地说)。
原因是当您调用 thread.join()
阻塞直到线程停止 运行 时,您实际上是在等待线程。当线程停止 运行 时,它会发出通知,以便解除对 join()
的所有调用者的阻塞。
由于您在线程上等待,当线程停止时您会隐式收到通知 运行。
这不是虚假唤醒,虚假唤醒是由 JVM 中的竞争条件引起的。这是您代码中的竞争条件。
println 使线程 1 保持活动状态的时间刚好足以使线程 2 可以在线程 1 终止之前开始等待。
线程 1 终止后,它会向等待其监视器的所有对象发送通知。 thread2 收到通知并停止等待。
删除 println 减少了线程 1 完成所需的时间,这样线程 1 已经完成,线程 2 可以开始等待它。线程 1 不再存在,并且在线程 2 开始等待之前它的通知已经发生,因此线程 2 永远等待。
中记录了线程在死亡时发送通知This implementation uses a loop of this.wait calls conditioned on this.isAlive. As a thread terminates the this.notifyAll method is invoked. It is recommended that applications not use wait, notify, or notifyAll on Thread instances.
(调用notifyAll的线程必须持有锁,如果其他线程抢到了锁,它可以保持终止线程存活并延迟notifyAll,直到终止线程可以获取锁。)
道德(好吧,道德之一)是始终在带有条件变量的循环中等待,参见 the Oracle tutorial。如果将 Thread2 更改为如下所示:
Thread t2 = new Thread() {
public void run()
{
try {
synchronized (t1) {
while (t1.isAlive()) {
t1.wait();
}
}
} catch (InterruptedException e) {
return;
}
System.out.println("Done.");
}
};
那么无论线程 2 是否可以在线程 1 完成之前开始等待,线程 2 都应该退出。
当然这是玩具示例的总范围:
不要扩展 Thread,使用 Runnable 或 Callable。
不要锁定线程。
不要启动线程,使用执行器。
比wait/notify更喜欢更高级别的并发结构。