在没有打印语句的情况下,循环看不到其他线程更改的值 [英] Loop doesn't see value changed by other thread without a print statement

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

问题描述

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

<小时>

以下是我的代码示例:

class MyHouse {布尔比萨到达 = 假;无效吃披萨(){while (pizzaArrived == false) {//System.out.println("等待");}System.out.println("真好吃!");}无效的交付比萨(){比萨到达=真;}}

while 循环运行时,我从不同的线程调用 deliverPizza() 来设置 pizzaArrived 变量.但是循环只在我取消注释 System.out.println("waiting"); 语句时才起作用.怎么回事?

解决方案

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

while (pizzaArrived == false) {}

进入这个:

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

这是一个无限循环.

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

 volatile 布尔比萨到达 = 假;

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

(更正式地说,volatile 在对变量的访问之间创建了 happens-before 关系.这意味着 一个线程在交付比萨之前所做的所有其他工作对于接收比萨的线程也是可见的,即使这些其他更改不会volatile 变量.)

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

class MyHouse {布尔比萨到达 = 假;无效吃披萨(){while (getPizzaArrived() == false) {}System.out.println("真好吃!");}同步布尔 getPizzaArrived() {返回披萨到了;}同步无效deliverPizza(){比萨到达=真;}}

<小时>

打印语句的效果

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

public void println(String x) {同步(这个){打印(x);新队();}}

同步防止 pizzaArrived 在循环期间被缓存.严格来说,两个线程必须在同一个对象上同步,以保证对变量的更改可见.(例如,在设置 pizzaArrived 之后调用 println 并在读取 pizzaArrived 之前再次调用它是正确的.)如果只有一个线程同步特定的对象,JVM 可以忽略它.在实践中,JVM 不够聪明,无法证明其他线程在设置 pizzaArrived 后不会调用 println,因此它假设它们可能会调用.因此,如果您调用 System.out.println,它无法在循环期间缓存变量.这就是为什么当它们有打印语句时像这样的循环可以工作,尽管它不是一个正确的修复.

使用 System.out 不是导致这种效果的唯一方法,但它是人们在尝试调试循环不工作的原因时最常发现的方法!<小时>

更大的问题

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

这里有一些方法可以做到这一点:

使用等待/通知

低级解决方案是使用等待/Object 的通知方法:

class MyHouse {布尔比萨到达 = 假;无效吃披萨(){同步(这个){而 (!pizzaArrived) {尝试 {this.wait();} catch (InterruptedException e) {}}}System.out.println("真好吃!");}无效的交付比萨(){同步(这个){比萨到达=真;this.notifyAll();}}}

在这个版本的代码中,循环线程调用了wait(),让线程进入睡眠状态.它在睡眠时不会使用任何 CPU 周期.第二个线程设置变量后,调用notifyAll() 唤醒正在等待该对象的任何/所有线程.这就像让披萨小哥按门铃,这样你就可以在等待的时候坐下来休息,而不是尴尬地站在门口.

当对一个对象调用wait/notify时,你必须持有该对象的同步锁,这就是上面代码所做的.你可以使用任何你喜欢的对象,只要两个线程使用相同的对象:这里我使用了 this(MyHouse 的实例).通常,两个线程不能同时进入同一个对象的同步块(这是同步目的的一部分)但它在这里工作是因为一个线程在 wait() 内部时临时释放同步锁 方法.

阻塞队列

A BlockingQueue用于实现生产者-消费者队列.消费者"从队列的前面拿取物品,生产者"将物品放在后面.一个例子:

class MyHouse {final BlockingQueuequeue = new LinkedBlockingQueue<>();voideatFood() 抛出 InterruptedException {//从队列中取出下一项(等待时休眠)对象食物 = queue.take();//并用它做一些事情System.out.println("饮食:" + 食物);}void deliveryPizza() 抛出 InterruptedException {//在生产者线程中,我们将项目推送到队列中.//如果队列中有空间,我们可以立即返回;//消费者线程稍后会访问它queue.put("美味的披萨");}}

注意:BlockingQueueputtake 方法可以抛出InterruptedException,它们是被检查的异常必须处理.在上面的代码中,为简单起见,重新抛出异常.您可能更喜欢捕获方法中的异常并重试 put 或 take 调用以确保它成功.除了那一点丑陋之外,BlockingQueue 非常好用.

这里不需要其他同步,因为 BlockingQueue 确保线程在将项目放入队列之前所做的一切对于取出这些项目的线程都是可见的.

执行者

Executors 就像现成的 BlockingQueues 执行任务.示例:

//一个SingleThreadExecutor"有一个工作线程和一个无限队列ExecutorService executor = Executors.newSingleThreadExecutor();RunnableeatPizza = () ->{ System.out.println("吃美味的披萨");};可运行的清理 = () ->{ System.out.println("打扫房子");};//我们提交将在工作线程上执行的任务executor.execute(eatPizza);executor.execute(cleanUp);//我们立即继续,无需等待任务完成

有关详细信息,请参阅 ExecutorExecutorServiceExecutors.

事件处理

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

JLabel label = new JLabel();JButton button = new JButton("点击我");button.addActionListener((ActionEvent e) -> {//该事件监听器在按钮被点击时运行.//我们不需要在等待时循环.label.setText("按钮被点击");});

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

JLabel label = new JLabel();JButton button = new JButton("计算答案");//为按钮添加一个点击监听器button.addActionListener((ActionEvent e) -> {//将 MyWorker 定义为结果类型为 String 的 SwingWorker:类 MyWorker 扩展了 SwingWorker{@覆盖公共字符串 doInBackground() 抛出异常 {//此方法在后台线程上调用.//你可以在这里做长时间的工作而不会阻塞 UI.//这只是一个例子:线程睡眠(5000);return "答案是 42";}@覆盖受保护的无效完成(){//一旦工作完成,这个方法就会在 Swing 线程上调用字符串结果;尝试 {结果 = 获取();} 捕获(异常 e){抛出新的运行时异常(e);}label.setText(结果);//将显示答案是 42"}}//启动worker新的 MyWorker().execute();});

定时器

要执行周期性操作,您可以使用 java.util.定时器.它比编写自己的计时循环更容易使用,并且更容易启动和停止.此演示每秒打印一次当前时间:

Timer timer = new Timer();TimerTask 任务 = 新的 TimerTask() {@覆盖公共无效运行(){System.out.println(System.currentTimeMillis());}};timer.scheduleAtFixedRate(task, 0, 1000);

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

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

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

其他方式

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

另请参阅 Java 教程的并发部分.多线程很复杂,但有很多可用的帮助!

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?


The following is an example of my code:

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 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?

解决方案

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) {}

into this:

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

which is an infinite loop.

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;

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.

(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.)

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;
    }
}


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();
    }
}

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.

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!


The bigger problem

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.

Here are some ways to do that:

Using 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();
        }
    }
}

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.

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.

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");
    }
}

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.

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.

Executors

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

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

Event handling

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");
});

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();
});

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);

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.

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:

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

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

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