首页 > 解决方案 > Java Thread 似乎跳过了条件语句

问题描述

对于我最近写的一个库,我写了一个无限循环的线程。在这个循环中,我从一个检查线程对象属性的条件语句开始。然而,似乎无论属性具有什么初始值,即使在更新后也将返回。

除非我做某种中断,例如Thread.sleep打印语句。

不幸的是,我不确定如何提出这个问题。否则我会查看 Java 文档。我已将代码简化为一个简单的示例,以简单的方式解释问题。

public class App {
    public static void main(String[] args) {
        App app = new App();
    }

    class Test implements Runnable {

        public boolean flag = false;

        public void run() {
            while(true) {

                // try {
                //     Thread.sleep(1);
                // } catch (InterruptedException e) {}

                if (this.flag) {
                    System.out.println("True");
                }
            }
        }

    }

    public App() {
        Test t = new Test();
        Thread thread = new Thread(t);
        System.out.println("Starting thread");
        thread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {}

        t.flag = true;
        System.out.println("New flag value: " + t.flag);
    }
}

现在,我假设在我们更改flag正在运行的线程上的属性值后,我们会立即看到大量的“True”向终端吐出。然而,我们不..

如果我取消注释Thread.sleep线程循环内的行,程序将按预期工作,并且在我们更改App对象中的值后,我们会看到打印出许多“True”行。另外,任何代替 的打印方法Thread.sleep也可以,但一些简单的赋值代码却不行。我认为这是因为它在编译时作为未使用的代码被提取出来。

所以,我的问题真的是:为什么我必须使用某种中断来让线程正确检查条件?

标签: javamultithreading

解决方案


所以,我的问题真的是:为什么我必须使用某种中断来让线程正确检查条件?

好吧,你不必。至少有两种方法可以在不使用“中断”的情况下实现这个特定示例。

  • 如果您声明flagvolatile,那么它将起作用。
  • 如果您声明flag为 be private,编写synchronizedgetter 和 setter 方法并将它们用于所有访问,它也将起作用。

    public class App {
        public static void main(String[] args) {
            App app = new App();
        }
    
        class Test implements Runnable {
            private boolean flag = false;
    
            public synchronized boolean getFlag() {
                return this.flag;
            }
    
            public synchronized void setFlag(boolean flag) {
                return this.flag = flag;
            }
    
            public void run() {
                while(true) {
                    if (this.getFlag()) {   // Must use the getter here too!
                        System.out.println("True");
                    }
                }
            }
        }
    
        public App() {
            Test t = new Test();
            Thread thread = new Thread(t);
            System.out.println("Starting thread");
            thread.start();
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
    
            t.setFlag(true);
            System.out.println("New flag value: " + t.getFlag());
    }
    

但是为什么你需要这样做呢?

因为除非您使用 avolatilesynchronized(并且您使用synchronized正确),否则不能保证一个线程看到另一个线程所做的内存更改。

在您的示例中,子线程看不到 的最新值flag。(这并不是说条件本身不正确或“不起作用”。他们实际上得到了陈旧的输入。这是“垃圾输入,垃圾输出”。)

Java 语言规范精确地规定了保证一个线程可以看到(以前的)另一个线程进行的写入的条件。规范的这一部分称为 Java 内存模型,它位于JLS 17.4中。Brian Goetz 等人在Java Concurrency in Practice中有一个更容易理解的解释。

请注意,意外行为可能是由于 JIT 决定将标志保留在寄存器中。也可能是 JIT 编译器决定它不需要强制内存缓存写入等。(JIT 编译器不希望对每个字段的每次内存写入都强制进行直写。这将对多核系统造成重大的性能影响……大多数现代机器都是如此。)


Java 中断机制是处理这个问题的另一种方法。您不需要任何同步,因为该方法会调用它。此外,当您尝试中断的线程当前正在等待或阻塞可中断操作时,中断将起作用;例如在Object::wait通话中。


推荐阅读