Java线程似乎跳过了条件语句

时间:2019-05-11 21:14:08

标签: java multithreading

对于我正在写的最新库,我写了一个无限循环的线程。在此循环中,我从条件语句开始检查线程对象上的属性。但是,无论该属性具有什么初始值,它都将是即使更新后仍会返回的值。

除非,我会进行一些打扰,例如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的打印方法,但某些简单的分配代码无效。我认为这是因为在编译时会将其作为未使用的代码拉出。

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

2 个答案:

答案 0 :(得分:3)

  

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

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

  • 如果您声明flagvolatile,那么它将起作用。
  • 如果您声明flagprivate,编写synchronized getter和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());
    }
    

但是您为什么需要这样做?

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

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

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

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


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

答案 1 :(得分:2)

由于未在该线程中修改变量,因此JVM可以自由有效地优化检查。要强制进行实际检查,请使用volatile关键字:

public volatile boolean flag = false;