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

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

问题描述

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


以下是我的代码示例:

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");语句时有效.发生了什么事?

解决方案

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

while (pizzaArrived == false) {}

对此:

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

这是一个无限循环.

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

volatile boolean pizzaArrived = false;

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

(更正式地说,volatile在变量访问之间创建先发生关系.这意味着同步方法主要用于实现互斥(防止发生两件事)相同的时间),但它们也具有与volatile相同的副作用.在读写变量时使用它们是使更改对其他线程可见的另一种方法:

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.outPrintStream对象. PrintStream的方法是这样同步的:

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

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

使用System.out并不是导致这种效果的唯一方法,而是人们在尝试调试其循环为什么不起作用时最常发现的一种方法!


更大的问题

while (pizzaArrived == false) {}是一个忙等待循环.那很糟!等待期间,它占用CPU,这会减慢其他应用程序的速度,并增加系统的电源使用,温度和风扇速度.理想情况下,我们希望循环线程在等待时进入睡眠状态,因此它不会占用CPU.

以下是一些方法:

使用等待/通知

一种低级解决方案是使用wait/通知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() 唤醒正在该对象上等待的所有/所有线程.这就像让比萨饼人按门铃一样,这样您就可以在等待时坐下来休息,而不必笨拙地站在门口.

在对一个对象调用wait/notify时,您必须持有该对象的同步锁,这就是上面的代码所做的.您可以使用任何您喜欢的对象,只要两个线程使用同一个对象即可:在这里,我使用了this(MyHouse的实例).通常,两个线程将无法同时输入同一对象的同步块(这是同步目的的一部分),但它在这里起作用,因为一个线程在wait()方法内部时会暂时释放同步锁. /p>

BlockingQueue

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

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

注意:BlockingQueueputtake方法会抛出InterruptedException,这是必须处理的检查异常.在上面的代码中,为简单起见,重新抛出了异常.您可能希望在方法中捕获异常,然后重试put或take调用以确保成功.除了这一点之外,BlockingQueue非常易于使用.

这里不需要其他同步,因为BlockingQueue确保将线程放入队列之前执行的所有操作对于将那些线程取出来的线程都是可见的.

执行者

Executor类似于执行任务的现成的BlockingQueue.示例:

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

有关详细信息,请参见 Executor 的文档, ExecutorService 在Swing中,例如:

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的其他交互,直到工作完成.慢速操作可以在新线程上启动,也可以使用上述一种技术(wait/notify,BlockingQueueExecutor)分派到等待的线程.您也可以使用 SwingWorker ,它是为此目的而专门设计的,并且会自动提供了一个后台工作线程:

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 .它比编写自己的定时循环更容易使用,并且更容易启动和停止.该演示每秒打印一次当前时间:

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.

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

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

其他方式

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

还请参见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天全站免登陆