实现 Runnable 而不是扩展 Thread 时的不同行为
Different behavior when implementing Runnable instead of extending Thread
这是代码。
基本上,如果我们更改 ReadCalculation 和 Calculator classes 以扩展 Thread 而不是实现 Runnable,我们将需要实例化这些 classes 并将它们传递给新的线程对象或仅调用它们的 start() 。
Calculator calc = new Calculator();
new ReadCalculation(calc).start();
new ReadCalculation(calc).start();
calc.start();
到目前为止没有什么特别的..但是当你执行这个小程序时,你的线程很可能会保持阻塞"Waiting for calculation..."如果我们通过扩展线程来检查 Runnable 实现class.
如果我们扩展 Thread class 而不是实现 Runnable,则行为是正确的,没有任何竞争条件的迹象。
有什么想法可能是这种行为的根源吗?
public class NotifyAllAndWait {
public static void main(String[] args) {
Calculator calc = new Calculator();
Thread th01 = new Thread(new ReadCalculation(calc));
th01.start();
Thread th02 = new Thread(new ReadCalculation(calc));
th02.start();
Thread calcThread = new Thread(calc);
calcThread.start();
}
}
class ReadCalculation implements Runnable {
private Calculator calc = null;
ReadCalculation(Calculator calc) {
this.calc = calc;
}
@Override
public void run() {
synchronized (calc) {
try {
System.out.println(Thread.currentThread().getName() + " Waiting for calculation...");
calc.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " Total: " + calc.getTotal());
}
}
}
class Calculator implements Runnable {
private int total = 0;
@Override
public void run() {
synchronized(this) {
System.out.println(Thread.currentThread().getName() + " RUNNING CALCULATION!");
for(int i = 0; i < 100; i = i + 2){
total = total + i;
}
notifyAll();
}
}
public int getTotal() {
return total;
}
}
当您执行 wait()
时,这需要在您执行 notify()
块的状态更改后处于循环中。例如
// when notify
changed = true;
x.notifyAll();
// when waiting
while(!changed)
x.wait();
如果不这样做,您将 运行 陷入诸如 wait() 虚假唤醒或 notify() 丢失等问题。
注意:一个线程可以很容易地在其他线程启动之前进行 100 次迭代。预创建 Thread
对象可能会对性能产生足够大的影响,从而改变您案例中的结果。
在 implements Runnable
版本中,至少,您不做任何事情来确保 ReadCalculation
线程在 Calculator
线程进入其 [=15] 之前到达 wait()
=] 块。如果 Calculator
线程首先进入其 synchronized
块,那么它将在 ReadCalculation
线程调用 wait()
之前调用 notifyAll()
。如果发生这种情况,那么 notifyAll()
是空操作,ReadCalculation
线程将永远等待。 (这是因为 notifyAll()
只关心 已经 等待对象的线程;它 不 设置任何类型的指示器可以通过后续调用 wait()
.)
检测到的对象
要解决这个问题,您可以在 Calculator
中添加一个 属性 以用于检查是否完成,并且仅在 Calculator
时才调用 wait()
未完成:
if(! calc.isCalculationDone()) {
calc.wait();
}
(请注意,为了完全避免竞争条件,重要的是整个 if
语句必须 在 块 synchronized
内, Calculator
将此 属性 设置在 调用 notifyAll()
的 synchronized
块中。你明白为什么了吗?)
(顺便说一句,Peter Lawrey 关于 "a thread can easily to your 100 iterations before the other threads have even started" 的评论具有很强的误导性,因为在您的程序中,100 次迭代全部发生在 after Calculator
已进入它的 synchronized
块。由于 ReadCalculation
线程被阻止进入它们的 synchronized
块并调用 calc.wait()
而 Calculator
在其 synchronized
块中,这是 1 次迭代、100 次迭代还是 1,000,000 次迭代无关紧要,除非它具有有趣的优化效果,可以改变程序的时间 before 那一点。)
您没有发布完整的 extends Thread
版本,但如果我正确理解它的样子,那么它实际上仍然具有相同的竞争条件。然而,由于竞争条件的本质,微小的变化会极大地影响不当行为的可能性。你仍然需要修复竞争条件,即使它似乎从来没有真正的行为不端,因为几乎可以肯定的是,如果你 运行 程序足够多次,它偶尔会行为不端。
我没有很好的解释为什么这种不当行为似乎在一种方法中比另一种方法更频繁地发生;但正如 user1643723 上面的评论,extends Thread
方法意味着很多代码 other 也可能锁定你的 Calculator
实例;这很可能会产生某种影响。但老实说,我认为不值得过分担心竞争条件可能更频繁或更不频繁地导致不当行为的原因。无论如何我们都必须修复它,所以,故事结束。
顺便提一句:
上面,我用了if(! calc.isCalculationDone())
;但实际上最好的做法是始终将对 wait()
的调用包装在适当的 while
循环中,因此实际上您应该编写 while(! calc.isCalculationDone())
。这主要有两个原因:
在非平凡的程序中,你不一定知道为什么 notifyAll()
被调用,或者即使你知道,你也不知道在等待线程实际唤醒并重新获得 synchronized
锁时,该原因是否仍然适用。如果你使用 while(not_ready_to_proceed()) { wait(); }
结构来表达 wait_until_ready_to_proceed()
的想法,而不是仅仅写 wait()
并试图确保在我们还没有准备好时不会有任何事情导致它 return。
在某些操作系统上,向进程发送信号将唤醒所有正在 wait()
-ing 的线程。这称为 虚假唤醒;有关详细信息,请参阅 "Do spurious wakeups actually happen?"。因此,即使没有其他线程调用 notify()
或 notifyAll()
.
,线程也可能会被唤醒
Calculator.run()
中的 for
循环不应该在 synchronized
块中,因为它不需要任何同步,所以争用是没有必要。在你的小程序中,它实际上并没有什么区别(因为 none 其他线程实际上在那个时候有任何事情要做),但最好的做法总是尽量减少里面的代码量synchronized
块。
这是代码。 基本上,如果我们更改 ReadCalculation 和 Calculator classes 以扩展 Thread 而不是实现 Runnable,我们将需要实例化这些 classes 并将它们传递给新的线程对象或仅调用它们的 start() 。
Calculator calc = new Calculator();
new ReadCalculation(calc).start();
new ReadCalculation(calc).start();
calc.start();
到目前为止没有什么特别的..但是当你执行这个小程序时,你的线程很可能会保持阻塞"Waiting for calculation..."如果我们通过扩展线程来检查 Runnable 实现class.
如果我们扩展 Thread class 而不是实现 Runnable,则行为是正确的,没有任何竞争条件的迹象。 有什么想法可能是这种行为的根源吗?
public class NotifyAllAndWait {
public static void main(String[] args) {
Calculator calc = new Calculator();
Thread th01 = new Thread(new ReadCalculation(calc));
th01.start();
Thread th02 = new Thread(new ReadCalculation(calc));
th02.start();
Thread calcThread = new Thread(calc);
calcThread.start();
}
}
class ReadCalculation implements Runnable {
private Calculator calc = null;
ReadCalculation(Calculator calc) {
this.calc = calc;
}
@Override
public void run() {
synchronized (calc) {
try {
System.out.println(Thread.currentThread().getName() + " Waiting for calculation...");
calc.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " Total: " + calc.getTotal());
}
}
}
class Calculator implements Runnable {
private int total = 0;
@Override
public void run() {
synchronized(this) {
System.out.println(Thread.currentThread().getName() + " RUNNING CALCULATION!");
for(int i = 0; i < 100; i = i + 2){
total = total + i;
}
notifyAll();
}
}
public int getTotal() {
return total;
}
}
当您执行 wait()
时,这需要在您执行 notify()
块的状态更改后处于循环中。例如
// when notify
changed = true;
x.notifyAll();
// when waiting
while(!changed)
x.wait();
如果不这样做,您将 运行 陷入诸如 wait() 虚假唤醒或 notify() 丢失等问题。
注意:一个线程可以很容易地在其他线程启动之前进行 100 次迭代。预创建 Thread
对象可能会对性能产生足够大的影响,从而改变您案例中的结果。
在 implements Runnable
版本中,至少,您不做任何事情来确保 ReadCalculation
线程在 Calculator
线程进入其 [=15] 之前到达 wait()
=] 块。如果 Calculator
线程首先进入其 synchronized
块,那么它将在 ReadCalculation
线程调用 wait()
之前调用 notifyAll()
。如果发生这种情况,那么 notifyAll()
是空操作,ReadCalculation
线程将永远等待。 (这是因为 notifyAll()
只关心 已经 等待对象的线程;它 不 设置任何类型的指示器可以通过后续调用 wait()
.)
要解决这个问题,您可以在 Calculator
中添加一个 属性 以用于检查是否完成,并且仅在 Calculator
时才调用 wait()
未完成:
if(! calc.isCalculationDone()) {
calc.wait();
}
(请注意,为了完全避免竞争条件,重要的是整个 if
语句必须 在 块 synchronized
内, Calculator
将此 属性 设置在 调用 notifyAll()
的 synchronized
块中。你明白为什么了吗?)
(顺便说一句,Peter Lawrey 关于 "a thread can easily to your 100 iterations before the other threads have even started" 的评论具有很强的误导性,因为在您的程序中,100 次迭代全部发生在 after Calculator
已进入它的 synchronized
块。由于 ReadCalculation
线程被阻止进入它们的 synchronized
块并调用 calc.wait()
而 Calculator
在其 synchronized
块中,这是 1 次迭代、100 次迭代还是 1,000,000 次迭代无关紧要,除非它具有有趣的优化效果,可以改变程序的时间 before 那一点。)
您没有发布完整的 extends Thread
版本,但如果我正确理解它的样子,那么它实际上仍然具有相同的竞争条件。然而,由于竞争条件的本质,微小的变化会极大地影响不当行为的可能性。你仍然需要修复竞争条件,即使它似乎从来没有真正的行为不端,因为几乎可以肯定的是,如果你 运行 程序足够多次,它偶尔会行为不端。
我没有很好的解释为什么这种不当行为似乎在一种方法中比另一种方法更频繁地发生;但正如 user1643723 上面的评论,extends Thread
方法意味着很多代码 other 也可能锁定你的 Calculator
实例;这很可能会产生某种影响。但老实说,我认为不值得过分担心竞争条件可能更频繁或更不频繁地导致不当行为的原因。无论如何我们都必须修复它,所以,故事结束。
顺便提一句:
上面,我用了
if(! calc.isCalculationDone())
;但实际上最好的做法是始终将对wait()
的调用包装在适当的while
循环中,因此实际上您应该编写while(! calc.isCalculationDone())
。这主要有两个原因:在非平凡的程序中,你不一定知道为什么
notifyAll()
被调用,或者即使你知道,你也不知道在等待线程实际唤醒并重新获得synchronized
锁时,该原因是否仍然适用。如果你使用while(not_ready_to_proceed()) { wait(); }
结构来表达wait_until_ready_to_proceed()
的想法,而不是仅仅写wait()
并试图确保在我们还没有准备好时不会有任何事情导致它 return。在某些操作系统上,向进程发送信号将唤醒所有正在
wait()
-ing 的线程。这称为 虚假唤醒;有关详细信息,请参阅 "Do spurious wakeups actually happen?"。因此,即使没有其他线程调用notify()
或notifyAll()
. ,线程也可能会被唤醒
Calculator.run()
中的for
循环不应该在synchronized
块中,因为它不需要任何同步,所以争用是没有必要。在你的小程序中,它实际上并没有什么区别(因为 none 其他线程实际上在那个时候有任何事情要做),但最好的做法总是尽量减少里面的代码量synchronized
块。