【问题标题】:Different behavior when implementing Runnable instead of extending Thread实现 Runnable 而不是扩展 Thread 时的不同行为
【发布时间】:2016-07-03 15:30:52
【问题描述】:

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

【问题讨论】:

  • 我不能引用语言或 JVM 规范的任何部分,这可能在这里起作用,但最可能的实际原因是 Thread 的源代码在 @ 987654324@ 和当前Thread 实例。您可以从calc 的内部和外部锁定计算器实例。当您使用专用目标 Runnable 时,这些锁可能不会干扰 Thread 内部的锁定(刚刚检查过:Android Thread 实现中的代码似乎不会锁定传递的 Runnable),但当您扩展 Thread 时肯定会影响执行顺序。跨度>

标签: java multithreading runnable race-condition synchronized


【解决方案1】:

至少在implements Runnable 版本中,您没有做任何事情来确保ReadCalculation 线程在Calculator 线程进入其synchronized 块之前到达wait()。如果Calculator 线程首先进入其synchronized 块,那么它将在ReadCalculation 线程调用wait() 之前调用notifyAll()。如果发生这种情况,那么notifyAll() 是无操作的,ReadCalculation 线程将永远等待。 (这是因为notifyAll() 只关心已经等待对象的线程;它确实在对象上设置任何可以被后续检测到的指示符致电wait()。)

要解决这个问题,您可以向Calculator 添加一个可用于检查是否已完成的属性,并且仅在Calculator 未完成时才调用wait() /p>

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

(请注意,为了完全避免竞争条件,重要的是整个if-statement 必须synchronized 块内,并且Calculator 设置此属性在调用notifyAll()synchronized块内。你明白为什么吗?)

(顺便说一句,Peter Lawrey 的评论“一个线程可以在其他线程甚至开始之前轻松地进行 100 次迭代”具有高度误导性,因为在您的程序中,100 次迭代都发生在之后 Calculator 已进入其synchronized 块。由于ReadCalculation 线程被阻止进入其synchronized 块并调用calc.wait()Calculator 在其synchronized 块中,因此这是否无关紧要是 1 次迭代、100 次迭代或 1,000,000 次迭代,除非它具有有趣的优化效果,可以在该点之前更改程序的时序。)


您尚未发布完整的extends Thread 版本,但如果我正确理解它的外观,那么它实际上仍然具有相同的竞争条件。但是,由于竞争条件的性质,微小的变化会极大地影响不当行为的可能性。你仍然需要修复竞态条件,即使它似乎从未真正出现异常,因为几乎可以肯定,如果你运行程序足够多次,它偶尔会出现异常。

我无法很好地解释为什么在一种方法中这种不当行为似乎比另一种方法更频繁地发生;但正如上面的 user1643723 cmets,extends Thread 方法意味着许多代码 other 也可能锁定您的 Calculator 实例;这很可能会产生某种影响。但老实说,我认为不值得过分担心比赛条件可能更频繁或更频繁​​地导致不当行为的原因。我们必须解决它,所以,故事的结尾。


顺便说一句:

  • 上面,我用了if(! calc.isCalculationDone());但实际上最好的做法是始终将对wait() 的调用包装在适当的while 循环中,所以你真的应该写while(! calc.isCalculationDone())。这有两个主要原因:

    • 在非平凡的程序中,你不一定知道 为什么 notifyAll() 被调用,或者即使你知道,你也不知道那个原因是否仍然适用等待线程实际上已经唤醒并重新获得了synchronized-lock。如果您使用while(not_ready_to_proceed()) { wait(); } 结构来表达wait_until_ready_to_proceed() 的想法,而不是仅仅编写wait() 并尝试确保当我们还没有准备好时,任何事情都不会导致它返回。

    • 在某些操作系统上,向进程发送信号将唤醒所有wait()-ing 线程。这称为虚假唤醒;请参阅"Do spurious wakeups actually happen?" 了解更多信息。因此,即使没有其他线程调用notify()notifyAll(),也可能会唤醒一个线程。

  • Calculator.run() 中的for 循环不应该在synchronized 块中,因为它不需要任何同步,因此不需要争用。在您的小程序中,它实际上并没有什么不同(因为此时其他线程实际上都没有任何事情要做),但最佳实践始终是尽量减少 synchronized 块内的代码量.

【讨论】:

    【解决方案2】:

    当您执行wait() 时,这需要在您执行notify() 块的状态更改后处于循环中。例如

    // when notify
    changed = true;
    x.notifyAll();
    
    // when waiting
    while(!changed)
        x.wait();
    

    如果您不这样做,您将遇到诸如 wait() 虚假唤醒或 notify() 丢失等问题。

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

    【讨论】:

    • 尽可能正确,您的第一句话在这里看起来无关紧要。
    • @user1643723 很高兴得到一个更好的答案来解释 OPs 问题。
    • @PeterLawrey 是否愿意详细说明“通知丢失”部分? JLS chapter about notify() 没有提到类似的事情。
    • @user1643723 Javadoc 声明docs.oracle.com/javase/8/docs/api/java/lang/…“唤醒所有在此对象的监视器上等待的线程。”所以如果没有线程在等待,什么都不会发生。同样 notify() 状态“如果有任何线程正在等待这个对象,则选择其中一个被唤醒。”如果您在没有任何等待时通知(),则不会更改任何状态。之后的任何 wait() 都可以永远 wait()。
    • 当然,这里没有人反对。但问题是问为什么在一种特定情况下,死锁似乎没有发生。您的陈述的第一部分听起来像是虚假唤醒可能是原因,但为什么代码的行为会根据小的代码更改而有所不同? AFAIK,可能有 OS-level and hardware-level 导致虚假唤醒的原因,例如接收 Linux 信号等。这些不完全符合所描述的模式。
    猜你喜欢
    • 2016-11-09
    • 2011-02-16
    • 1970-01-01
    • 2013-05-05
    • 2014-06-02
    • 2012-02-15
    • 1970-01-01
    • 2018-01-10
    • 1970-01-01
    相关资源
    最近更新 更多