实现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()。如果发生这种情况,那么notifyAll()是无操作的,ReadCalculation线程将永远等待。(这是因为notifyAll()只关心那些已经等待对象的线程;没有在对象上设置任何类型的指示器,这些指示器可以被随后对wait()的调用检测到。)

要解决这个问题,您可以添加一个属性到Calculator,可以用来检查是否完成,并且只有当Calculator而不是 done时才调用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());但它实际上是一个最佳实践,总是包装调用wait()在一个适当的while循环,所以你真的应该写while(! calc.isCalculationDone())。这主要有两个原因:

    • 在重要的程序中,您不一定知道为什么调用 notifyAll(),或者即使您知道,您也不知道等待线程实际唤醒并重新获得synchronized锁时该原因是否仍然适用。如果您使用while(not_ready_to_proceed()) { wait(); }结构来表达wait_until_ready_to_proceed()的思想,而不仅仅是编写wait()并试图确保在我们还没有准备好时没有任何东西会导致它返回,则可以更容易地推断notify()/wait()交互的正确性。

    • 在某些操作系统上,向进程发送信号将唤醒所有正在wait() -ing的线程。这被称为伪唤醒;更多信息请参见"虚假唤醒真的会发生吗?"因此,即使没有其他线程调用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对象可能会对性能产生足够的影响,从而改变结果。

最新更新