没有print语句,循环看不到更改的值 [英] Loop doesn't see changed value without a print statement

查看:200
本文介绍了没有print语句,循环看不到更改的值的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我的代码中,我有一个循环,等待某个状态从另一个线程更改。另一个线程工作,但我的循环永远不会看到更改的值。 它永远等待。然而,当我在循环中放入一个 System.out.println 语句时,它突然起作用了!为什么?

In my code I have a loop that waits for some state to be changed from a different thread. The other thread works, but my loop never sees the changed value. It waits forever. However, when I put a System.out.println statement in the loop, it suddenly works! Why?

以下是我的代码示例:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (pizzaArrived == false) {
            //System.out.println("waiting");
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        pizzaArrived = true;
    }
}

当while循环运行时,我调用 deliverPizza()从另一个线程设置 pizzaArrived 变量。但循环仅在我取消注释 System.out.println(waiting); 语句时才有效。发生了什么?

While the while loop is running, I call deliverPizza() from a different thread to set the pizzaArrived variable. But the loop only works when I uncomment the System.out.println("waiting"); statement. What's going on?

推荐答案

允许JVM假设其他线程不会更改 pizzaArrived 循环期间的变量。换句话说,它可以在循环外提升 pizzaArrived == false 测试,优化它:

The JVM is allowed to assume that other threads do not change the pizzaArrived variable during the loop. In other words, it can hoist the pizzaArrived == false test outside the loop, optimizing this:

while (pizzaArrived == false) {}

进入:

if (pizzaArrived == false) while (true) {}

这是一个无限循环。

为确保一个线程所做的更改对其他线程可见,您必须总是在线程之间添加一些同步。最简单的方法是使共享变量 volatile

To ensure that changes made by one thread are visible to other threads you must always add some synchronization between the threads. The simplest way to do this is to make the shared variable volatile:

volatile boolean pizzaArrived = false;

使变量 volatile 保证不同的线程将看到彼此的变化对它的影响。这可以防止JVM缓存 pizzaArrived 的值或在循环外提升测试。相反,它必须每次都读取实变量的值。

Making a variable volatile guarantees that different threads will see the effects of each other's changes to it. This prevents the JVM from caching the value of pizzaArrived or hoisting the test outside the loop. Instead, it must read the value of the real variable every time.

(更正式地, volatile 创建一个在访问变量之前发生关系。这意味着 all在交付比萨饼之前,线程所做的其他工作也可以在接收披萨的线程中看到,即使那些其他更改不是 volatile 变量。)

(More formally, volatile creates a happens-before relationship between accesses to the variable. This means that all other work a thread did before delivering the pizza is also visible to the thread receiving the pizza, even if those other changes are not to volatile variables.)

主要使用同步方法实现互斥(防止两件事同时发生),但它们也具有 volatile 所具有的所有副作用。在读取和写入变量时使用它们是另一种使更改对其他线程可见的方法:

Synchronized methods are used principally to implement mutual exclusion (preventing two things happening at the same time), but they also have all the same side-effects that volatile has. Using them when reading and writing a variable is another way to make the changes visible to other threads:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (getPizzaArrived() == false) {}
        System.out.println("That was delicious!");
    }

    synchronized boolean getPizzaArrived() {
        return pizzaArrived;
    }

    synchronized void deliverPizza() {
        pizzaArrived = true;
    }
}






打印语句的效果



System.out PrintStream 对象。 PrintStream 的方法是这样同步的:


The effect of a print statement

System.out is a PrintStream object. The methods of PrintStream are synchronized like this:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

同步阻止 pizzaArrived 在循环期间被缓存。 严格地说,两个线程必须在同一个对象上同步,以保证对变量的更改是可见的。 (例如,在设置 pizzaArrived 之后调用 println 并在阅读 pizzaArrived之前再次调用它是正确的。)如果只有一个线程在特定对象上同步,则允许JVM忽略它。实际上,在设置 pizzaArrived 之后,JVM不够聪明,无法证明其他线程不会调用 println ,所以它假设他们可能。因此,如果调用 System.out.println ,则它无法在循环期间缓存变量。这就是为什么这样的循环在有打印语句时有效,尽管它不是正确的解决方法。

The synchronization prevents pizzaArrived being cached during the loop. Strictly speaking, both threads must synchronize on the same object to guarantee that changes to the variable are visible. (For example, calling println after setting pizzaArrived and calling it again before reading pizzaArrived would be correct.) If only one thread synchronizes on a particular object, the JVM is allowed to ignore it. In practice, the JVM is not smart enough to prove that other threads won't call println after setting pizzaArrived, so it assumes that they might. Therefore, it cannot cache the variable during the loop if you call System.out.println. That's why loops like this work when they have a print statement, although it is not a correct fix.

使用 System.out 不是导致这种效果的唯一方法,但它是人们最常发现的,当他们试图调试为什么他们的循环不起作用时!

Using System.out is not the only way to cause this effect, but it is the one people discover most often, when they are trying to debug why their loop doesn't work!

while(pizzaArrived == false){} 是一个忙等待循环。那很糟!当它等待时,它会占用CPU,从而减慢其他应用程序的速度,并增加系统的功耗,温度和风扇速度。理想情况下,我们希望循环线程在等待时休眠,因此它不会占用CPU。

while (pizzaArrived == false) {} is a busy-wait loop. That's bad! While it waits, it hogs the CPU, which slows down other applications, and increases the power usage, temperature, and fan speed of the system. Ideally, we would like the loop thread to sleep while it waits, so it does not hog the CPU.

以下是一些方法:

低级解决方案是使用 Object 的wait / notify方法:

A low-level solution is to use the wait/notify methods of Object:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        synchronized (this) {
            while (!pizzaArrived) {
                try {
                    this.wait();
                } catch (InterruptedException e) {}
            }
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        synchronized (this) {
            pizzaArrived = true;
            this.notifyAll();
        }
    }
}

在此版本的代码中,循环线程调用 wait() ,这使线程处于睡眠状态。睡觉时不会使用任何CPU周期。第二个线程设置变量后,它调用 notifyAll() 唤醒等待该对象的所有/所有线程。这就像披萨家伙敲响了门铃一样,所以你可以坐下来休息,等待,而不是笨拙地站在门口。

In this version of the code, the loop thread calls wait(), which puts the thread the sleep. It will not use any CPU cycles while sleeping. After the second thread sets the variable, it calls notifyAll() to wake up any/all threads which were waiting on that object. This is like having the pizza guy ring the doorbell, so you can sit down and rest while waiting, instead of standing awkwardly at the door.

当呼叫等待/通知时一个对象,你必须持有该对象的同步锁,这是上面的代码所做的。你可以使用你喜欢的任何对象,只要两个线程使用相同的对象:这里我使用这个 MyHouse的实例)。通常,两个线程无法同时进入同一对象的同步块(这是同步目的的一部分),但它可以在这里工作,因为当一个线程位于内时,它会临时释放同步锁wait()方法。

When calling wait/notify on an object you must hold the synchronization lock of that object, which is what the above code does. You can use any object you like so long as both threads use the same object: here I used this (the instance of MyHouse). Usually, two threads would not be able to enter synchronized blocks of the same object simultaneously (which is part of the purpose of synchronization) but it works here because a thread temporarily releases the synchronization lock when it is inside the wait() method.

A BlockingQueue 用于实施生产者 - 消费者队列。 消费者从队列的前面取物品,生产者在后面推动物品。例如:

A BlockingQueue is used to implement producer-consumer queues. "Consumers" take items from the front of the queue, and "producers" push items on at the back. An example:

class MyHouse {
    final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();

    void eatFood() throws InterruptedException {
        // take next item from the queue (sleeps while waiting)
        Object food = queue.take();
        // and do something with it
        System.out.println("Eating: " + food);
    }

    void deliverPizza() throws InterruptedException {
        // in producer threads, we push items on to the queue.
        // if there is space in the queue we can return immediately;
        // the consumer thread(s) will get to it later
        queue.put("A delicious pizza");
    }
}

注意: put 获取 的方法> BlockingQueue 可以抛出 InterruptedException s,这是必须处理的已检查异常。在上面的代码中,为简单起见,重新抛出了异常。您可能更喜欢捕获方法中的异常并重试put或take调用以确保它成功。除了这一点之外, BlockingQueue 非常易于使用。

Note: The put and take methods of BlockingQueue can throw InterruptedExceptions, which are checked exceptions which must be handled. In the above code, for simplicity, the exceptions are rethrown. You might prefer to catch the exceptions in the methods and retry the put or take call to be sure it succeeds. Apart from that one point of ugliness, BlockingQueue is very easy to use.

此处不需要其他同步,因为一个 BlockingQueue 确保在将项目放入队列之前所做的所有线程对于获取这些项目的线程是可见的。

No other synchronization is needed here because a BlockingQueue makes sure that everything threads did before putting items in the queue is visible to the threads taking those items out.

执行者 s就像现成的 BlockingQueue 执行任务的s。例如:

Executors are like ready-made BlockingQueues which execute tasks. Example:

// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();

Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };

// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish

详情请见 执行者 ExecutorService 执行人

For details see the doc for Executor, ExecutorService, and Executors.

在等待时循环用户在UI中单击某些内容是错误的。而是使用UI工具包的事件处理功能。 在Swing中,例如:

Looping while waiting for the user to click something in a UI is wrong. Instead, use the event handling features of the UI toolkit. In Swing, for example:

JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
    // This event listener is run when the button is clicked.
    // We don't need to loop while waiting.
    label.setText("Button was clicked");
});

因为事件处理程序在事件调度线程上运行,所以在事件处理程序中进行长时间的工作会阻止其他交互使用UI直到工作完成。可以在新线程上启动慢速操作,或使用上述技术之一调度到等待线程(等待/通知, BlockingQueue 执行)。您还可以使用 SwingWorker ,专为此设计,并自动提供后台工作线程:

Because the event handler runs on the event dispatch thread, doing long work in the event handler blocks other interaction with the UI until the work is finished. Slow operations can be started on a new thread, or dispatched to a waiting thread using one of the above techniques (wait/notify, a BlockingQueue, or Executor). You can also use a SwingWorker, which is designed exactly for this, and automatically supplies a background worker thread:

JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");

// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {

    // Defines MyWorker as a SwingWorker whose result type is String:
    class MyWorker extends SwingWorker<String,Void> {
        @Override
        public String doInBackground() throws Exception {
            // This method is called on a background thread.
            // You can do long work here without blocking the UI.
            // This is just an example:
            Thread.sleep(5000);
            return "Answer is 42";
        }

        @Override
        protected void done() {
            // This method is called on the Swing thread once the work is done
            String result;
            try {
                result = get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            label.setText(result); // will display "Answer is 42"
        }
    }

    // Start the worker
    new MyWorker().execute();
});



计时器



要执行定期操作,你可以使用 java.util.Timer 。它比编写自己的定时循环更容易使用,更容易启动和停止。该演示每秒打印一次当前时间:

Timers

To perform periodic actions, you can use a java.util.Timer. It is easier to use than writing your own timing loop, and easier to start and stop. This demo prints the current time once per second:

Timer timer = new Timer();
TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println(System.currentTimeMillis());
    }
};
timer.scheduleAtFixedRate(task, 0, 1000);

每个 java.util.Timer 都有它的自己的后台线程,用于执行其预定的 TimerTask 。当然,线程在任务之间休眠,因此它不会占用CPU。

Each java.util.Timer has its own background thread which is used to execute its scheduled TimerTasks. Naturally, the thread sleeps between tasks, so it does not hog the CPU.

在Swing代码中,还有一个 javax.swing.Timer ,它类似,但它执行监听器Swing线程,因此您可以安全地与Swing组件交互而无需手动切换线程:

In Swing code, there is also a javax.swing.Timer, which is similar, but it executes the listener on the Swing thread, so you can safely interact with Swing components without needing to manually switch threads:

JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
    frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);



其他方式



如果您正在写作多线程代码,值得探索这些包中的类以查看可用的内容:

Other ways

If you are writing multithreaded code, it is worth exploring the classes in these packages to see what is available:

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

还可以看到并发部分。多线程很复杂,但有很多帮助可用!

And also see the Concurrency section of the Java tutorials. Multithreading is complicated, but there is lots of help available!

这篇关于没有print语句,循环看不到更改的值的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆