实现Runnable而不是扩展Thread时的不同行为

时间:2016-07-03 15:30:53

标签: java multithreading runnable race-condition synchronized

所以这是代码。 基本上,如果我们更改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;
    }
}

2 个答案:

答案 0 :(得分:2)

执行wait()时,需要在状态更改后循环执行notify()块。 e.g。

// when notify
changed = true;
x.notifyAll();

// when waiting
while(!changed)
    x.wait();

如果你不这样做,你将遇到问题,例如wait()虚假地醒来或通知()丢失。

注意:在其他线程开始之前,线程可以很容易地进行100次迭代。预先创建Thread对象可能会对性能产生足够的影响,从而改变您的结果。

答案 1 :(得分:2)

至少在implements Runnable版本中,您无法确保ReadCalculation线程在wait()线程进入其Calculator之前到达synchronized块。如果Calculator线程首先进入其synchronized块,则它会在notifyAll()线程调用ReadCalculation之前调用wait()。如果发生这种情况,那么notifyAll()就是无操作,而ReadCalculation线程将永远等待。 (这是因为notifyAll()只关心已经等待某个对象的线程;它设置可以检测到的对象上的任何类型的指示符随后调用wait()。)

要解决此问题,您可以向Calculator添加可用于检查已完成状态的属性,并且只有在wait() 不<时才调用Calculator / em>完成:

if(! calc.isCalculationDone()) {
    calc.wait();
}

(注意,为了完全避免竞争条件,重要的是整个if - 语句 synchronized块内,{{1}设置此属性里面调用Calculator的{​​{1}}块。你知道为什么吗?)

(顺便说一下,Peter Lawrey的评论“在其他线程开始之前,线程可以很容易地进行100次迭代”是非常误导的,因为在你的程序中,100次迭代都发生在之后 synchronized已进入其notifyAll()区块。由于Calculator个帖子被阻止进入synchronized个区块,并且ReadCalculation号位于synchronized calc.wait()阻止,无论这是1次迭代,100次迭代还是1,000,000次迭代都无关紧要,除非它具有有趣的优化效果,可以在之前改变程序的时间。 )

您还没有发布整个Calculator版本,但如果我理解它的外观,那么它实际上仍然具有相同的竞争条件。然而,在竞争条件的本质中,微小的变化会严重影响不良行为的可能性。你仍然需要修复竞争条件,即使它似乎从来没有真正行为不端,因为如果你运行程序足够多次,它几乎可以肯定会偶尔发生错误。

我没有一个很好的解释,为什么这种不良行为似乎比一种方法更频繁地发生;但正如上面的user1643723评论所述,synchronized方法意味着许多代码其他比你的还可能锁定在你的extends Thread实例上;这很可能会产生某种影响。但老实说,我认为值得担心的原因是竞争条件可能会更频繁或更不频繁地导致不良行为;无论如何,我们必须解决它。故事结束。

顺便提及:

  • 上面,我使用了extends Thread;但实际上最好总是在适当的Calculator循环中包含对if(! calc.isCalculationDone())的调用,所以你应该写wait()。这有两个主要原因:

    • 在非平凡的程序中,您不一定知道为什么 while被调用,或者即使您这样做,也不知道该原因是否仍然适用于等待线程实际唤醒并重新获得while(! calc.isCalculationDone()) - 锁定的时间。如果您使用notifyAll()结构来表达synchronized的想法,而不仅仅是写{{} notify(),那么您可以更轻松地推断wait() / while(not_ready_to_proceed()) { wait(); }互动的正确性{1}}并尝试确保在我们还没准备好的时候什么都不会导致它返回。

    • 在某些操作系统上,向进程发送信号将唤醒所有wait_until_ready_to_proceed()个线程。这称为虚假唤醒;有关详细信息,请参阅"Do spurious wakeups actually happen?"。因此,即使没有其他线程称为wait()wait(),线程也可能被唤醒。

  • notify()中的notifyAll() - 循环不应位于for块中,因为它不需要任何同步,因此不需要进行争用。在你的小程序中,它实际上并没有什么区别(因为其他线程实际上没有任何事情要做),但最好的做法是尽量减少{{1}内的代码量。 } blocks。