在没有打印语句的情况下,循环看不到其他线程更改的值 [英] Loop doesn't see value changed by other thread without a print statement
问题描述
在我的代码中,我有一个循环等待某个状态从不同的线程改变.另一个线程有效,但我的循环从未看到更改的值.它永远等待. 然而,当我在循环中放入一个 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 BlockingQueue
注意:BlockingQueue
的put
和take
方法可以抛出InterruptedException
,它们是被检查的异常必须处理.在上面的代码中,为简单起见,重新抛出异常.您可能更喜欢捕获方法中的异常并重试 put 或 take 调用以确保它成功.除了那一点丑陋之外,BlockingQueue
非常好用.
这里不需要其他同步,因为 BlockingQueue
确保线程在将项目放入队列之前所做的一切对于取出这些项目的线程都是可见的.
执行者
Executor
s 就像现成的 BlockingQueue
s 执行任务.示例:
//一个SingleThreadExecutor"有一个工作线程和一个无限队列ExecutorService executor = Executors.newSingleThreadExecutor();RunnableeatPizza = () ->{ System.out.println("吃美味的披萨");};可运行的清理 = () ->{ System.out.println("打扫房子");};//我们提交将在工作线程上执行的任务executor.execute(eatPizza);executor.execute(cleanUp);//我们立即继续,无需等待任务完成
有关详细信息,请参阅 Executor
、ExecutorService
和 Executors
.
事件处理
在等待用户单击 UI 中的某些内容时循环是错误的.相反,请使用 UI 工具包的事件处理功能.在 Swing 中,例如:
JLabel label = new JLabel();JButton button = new JButton("点击我");button.addActionListener((ActionEvent e) -> {//该事件监听器在按钮被点击时运行.//我们不需要在等待时循环.label.setText("按钮被点击");});
因为事件处理程序运行在事件调度线程上,所以在事件处理程序中进行长时间的工作会阻止与 UI 的其他交互,直到工作完成.慢速操作可以在新线程上启动,或者使用上述技术之一(等待/通知、BlockingQueue
或 Executor
)分派到等待线程.您还可以使用 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 InterruptedException
s, 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
Executor
s are like ready-made BlockingQueue
s 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 TimerTask
s. 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屋!