实现Runnable而不是扩展Thread时的不同行为
所以这是代码。 基本上,如果我们将ReadCalculation和Calculator类更改为扩展Thread而不是实现Runnable,则需要实例化这些类并将它们传递给一个新的线程对象,或者仅对它们调用start()。
Calculator calc = new Calculator();
new ReadCalculation(calc).start();
new ReadCalculation(calc).start();
calc.start();
目前为止没有什么特别的..但是当你执行这个小程序时,如果我们要通过扩展Thread类来检查Runnable实现,那么线程将很容易被阻塞“等待计算......”。
如果我们扩展Thread类而不是实现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;
}
}
在implements Runnable
版本中,至少,您无需确保ReadCalculation
线程在Calculator
线程进入其synchronized
块之前到达wait()
。 如果Calculator
线程首先进入其synchronized
块,那么它将在ReadCalculation
线程调用wait()
之前调用notifyAll()
wait()
。 如果发生这种情况,那么notifyAll()
是一个无操作,并且ReadCalculation
线程将永远等待。 (这是因为notifyAll()
只关心已经在一个对象上等待的线程;它不会在对象上设置任何类型的指示符,这些指示符可能会被随后的wait()
调用检测到。
为了解决这个问题,你可以添加一个属性到Calculator
,它可以用来检查是否完成,并且只有在Calculator
没有完成时才调用wait()
:
if(! calc.isCalculationDone()) {
calc.wait();
}
(请注意,为了完全避免竞争条件,重要的是将整个if
语句放在synchronized
块中,并且Calculator
在调用notifyAll()
的synchronized
块内部设置此属性。你明白为什么?)
(顺便说一句,Peter Lawrey的评论“在其他线程已经开始之前,线程可以轻松地进行100次迭代”具有高度的误导性,因为在你的程序中,100次迭代都发生在Calculator
进入其synchronized
块之后,因为ReadCalculation
线程被阻止进入它们的synchronized
块,并在Calculator
处于其synchronized
块时调用calc.wait()
,它应该是1次迭代,100次迭代还是1,000,000次迭代无关紧要,除非它具有可能会改变的有趣优化效果该点之前的计划的时间。)
您尚未发布整个extends Thread
版本,但如果我正确理解它的外观,那么它实际上仍然具有相同的竞争条件。 然而,根据竞争条件的性质,微小的变化可能会严重影响不正当行为的可能性。 即使它看起来从来没有出现过错,仍然需要修正竞争条件,因为几乎可以肯定,如果您运行该程序的次数足够多,它会偶尔会出现不正常行为。
我没有一个很好的解释,为什么这种不正当行为似乎更经常地以一种方式发生,而不是另一种; 但正如上面的user1643723注释, extends Thread
方法意味着除您之外的大量代码也可能锁定您的Calculator
实例; 这可能会有某种效果。 但说实话,我认为值得担忧的是,为什么竞争条件会更频繁地或不经常地引起不良行为; 无论如何,我们必须解决它,故事的结尾。
顺便:
上面,我用if(! calc.isCalculationDone())
; 但实际上最好的做法是始终在适当的while
while(! calc.isCalculationDone())
调用包装为wait()
,所以你应该写while(! calc.isCalculationDone())
。 这主要有两个原因:
在非平凡的程序中,你不一定知道为什么notifyAll()
被调用,或者即使你这样做,也不知道在等待线程实际唤醒并恢复synchronized
锁定时,该原因是否仍然适用。 如果您使用while(not_ready_to_proceed()) { wait(); }
,则可以更轻松地while(not_ready_to_proceed()) { wait(); }
notify()
/ wait()
交互的正确性while(not_ready_to_proceed()) { wait(); }
while(not_ready_to_proceed()) { wait(); }
结构来表达wait_until_ready_to_proceed()
的思想,而不是只写wait()
并试图确保没有任何东西会导致它在我们没有准备好时返回。
在某些操作系统上,向进程发送信号将唤醒所有wait()
线程。 这被称为虚假唤醒; 请参阅“真正发生虚假唤醒?” 了解更多信息。 因此,即使没有其他线程调用notify()
或notifyAll()
线程也可能会被唤醒。
Calculator.run()
的for
-loop不应位于synchronized
块中,因为它不需要任何同步,因此不需要争用。 在你的小程序中,它实际上并没有什么区别(因为无论如何,其他线程实际上没有任何事情要做),但最好的做法是尽量减少synchronized
块内的代码量。
当你执行一个wait()
时,你需要在执行notify()
模块的状态改变后进入一个循环。 例如
// when notify
changed = true;
x.notifyAll();
// when waiting
while(!changed)
x.wait();
如果你不这样做,你将遇到诸如wait()虚假地唤醒或notify()丢失等问题。
注意:在其他线程开始之前,线程可以很容易地进行100次迭代。 预先创建Thread
对象可能会对性能产生不同影响,以改变您的案例中的结果。
上一篇: Different behavior when implementing Runnable instead of extending Thread