Цикл не видит измененное значение без оператора печати
В моем коде у меня есть цикл, который ожидает изменения состояния из другого потока. Другой поток работает, но мой цикл никогда не видит измененное значение. Это ждет вечно. Тем не менее, когда я ставлю 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");
заявление. В чем дело?
1 ответ
JVM разрешается предполагать, что другие потоки не изменяют pizzaArrived
переменная во время цикла. Другими словами, он может поднять pizzaArrived == false
тест вне цикла, оптимизируя это:
while (pizzaArrived == false) {}
в это:
if (pizzaArrived == false) while (true) {}
который представляет собой бесконечный цикл.
Чтобы изменения, сделанные одним потоком, были видны другим потокам, вы всегда должны добавлять некоторую синхронизацию между потоками. Самый простой способ сделать это - создать переменную общего доступа. volatile
:
volatile boolean pizzaArrived = false;
Создание переменной volatile
гарантирует, что разные потоки увидят влияние изменений друг друга. Это предотвращает кэширование JVM значения pizzaArrived
или поднять тест вне цикла. Вместо этого он должен читать значение реальной переменной каждый раз.
(Более формально, volatile
создает отношение " происходит до" между обращениями к переменной. Это означает, что вся другая работа, выполненная потоком перед доставкой пиццы, также видна потоку, получающему пиццу, даже если эти другие изменения не относятся к 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.out
это PrintStream
объект. Методы PrintStream
синхронизируются так:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
Синхронизация мешает pizzaArrived
кэшируется во время цикла. Строго говоря, оба потока должны синхронизироваться на одном и том же объекте, чтобы гарантировать, что изменения в переменной видны. (Например, вызов println
после настройки pizzaArrived
и позвонив еще раз, прежде чем читать pizzaArrived
было бы правильно.) Если только один поток синхронизируется на конкретном объекте, JVM может игнорировать его. На практике JVM недостаточно умна, чтобы доказать, что другие потоки не будут вызывать println
после настройки pizzaArrived
, так что предполагается, что они могут. Поэтому он не может кэшировать переменную во время цикла, если вы вызываете System.out.println
, Вот почему такие циклы работают, когда у них есть оператор печати, хотя это не является правильным исправлением.
С помощью System.out
это не единственный способ вызвать этот эффект, но это тот, который люди чаще всего обнаруживают, когда пытаются отладить, почему их цикл не работает!
Большая проблема
while (pizzaArrived == false) {}
это цикл ожидания ожидания Это плохо! Во время ожидания он загружает процессор, что замедляет работу других приложений и увеличивает энергопотребление, температуру и скорость вращения вентилятора системы. В идеале мы хотели бы, чтобы поток цикла находился в спящем режиме, пока он ожидает, чтобы он не перегружал процессор.
Вот несколько способов сделать это:
Использование ожидания / уведомления
Низкоуровневым решением является использование методов ожидания / уведомления 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()
, который ставит нить в сон. Он не будет использовать циклы ЦП во время сна. После того, как второй поток устанавливает переменную, он вызывает notifyAll()
чтобы разбудить любые / все потоки, которые ожидали этого объекта. Это похоже на то, как пиццерия звонит в дверной звонок, так что вы можете сесть и отдохнуть в ожидании, а не неловко стоять у двери.
При вызове wait/notify для объекта вы должны удерживать блокировку синхронизации этого объекта, что и делает приведенный выше код. Вы можете использовать любой понравившийся вам объект, если оба потока используют один и тот же объект: здесь я использовал this
(случай MyHouse
). Обычно два потока не могут одновременно вводить синхронизированные блоки одного и того же объекта (что является частью цели синхронизации), но это работает здесь, потому что поток временно снимает блокировку синхронизации, когда он находится внутри wait()
метод.
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");
}
}
Обратите внимание put
а также take
методы BlockingQueue
может бросить InterruptedException
s, которые являются проверенными исключениями, которые должны быть обработаны. В приведенном выше коде, для простоты, исключения переброшены. Вы можете предпочесть перехватить исключения в методах и повторить вызов put или take, чтобы убедиться, что он успешен. Помимо этого одного уродства, BlockingQueue
очень прост в использовании.
Никакой другой синхронизации здесь не требуется, потому что BlockingQueue
удостоверяется, что все, что делали потоки перед помещением элементов в очередь, было видимым для потоков, выводящих эти элементы.
Исполнители
Executor
s как готовые 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
, а также Executors
,
Обработка событий
Цикл во время ожидания нажатия пользователем чего-либо в пользовательском интерфейсе неверен. Вместо этого используйте функции обработки событий инструментария пользовательского интерфейса. В 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");
});
Поскольку обработчик событий выполняется в потоке диспетчеризации событий, выполнение длительной работы в обработчике событий блокирует другое взаимодействие с пользовательским интерфейсом, пока работа не будет завершена. Медленные операции могут быть запущены в новом потоке или отправлены ожидающему потоку с использованием одного из вышеуказанных методов (ожидание / уведомление, BlockingQueue
, или же Executor
). Вы также можете использовать 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
s. Естественно, поток спит между задачами, поэтому он не перегружает процессор.
В коде 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. Многопоточность сложна, но есть много помощи!