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
A BlockingQueue
используется для реализации очередей производитель-потребитель. «Потребители» берут товары в начале очереди, а «производители» проталкивают товары сзади. Пример:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
Object food = queue.take();
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
queue.put("A delicious pizza");
}
}
Примечание. Методы put
и могут генерировать s, которые являются отмеченными исключениями, которые необходимо обработать. В приведенном выше коде для простоты исключения повторно генерируются. Вы можете предпочесть перехватить исключения в методах и повторить вызов put или take, чтобы убедиться, что он завершится успешно. Не считая этого уродства, пользоваться им очень просто.take
BlockingQueue
InterruptedException
BlockingQueue
Никакой другой синхронизации здесь не требуется, потому что a BlockingQueue
гарантирует, что все, что потоки сделали перед помещением элементов в очередь, видно потокам, извлекающим эти элементы.
Исполнители
Executor
s похожи на готовые BlockingQueue
s, которые выполняют задачи. Пример:
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
executor.execute(eatPizza);
executor.execute(cleanUp);
Более подробную информацию см в док для Executor
, ExecutorService
и Executors
.
Обработка событий
Зацикливание в ожидании, пока пользователь что-то щелкнет в пользовательском интерфейсе, неправильно. Вместо этого используйте функции обработки событий набора инструментов пользовательского интерфейса. В Swing , например:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
label.setText("Button was clicked");
});
Поскольку обработчик событий работает в потоке отправки событий, длительная работа в обработчике событий блокирует другое взаимодействие с пользовательским интерфейсом, пока работа не будет завершена. Медленные операции могут быть запущены в новом потоке или отправлены в ожидающий поток с помощью одного из вышеуказанных методов (ожидание / уведомление, a BlockingQueue
или Executor
). Вы также можете использовать SwingWorker
, который предназначен именно для этого и автоматически предоставляет фоновый рабочий поток:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
button.addActionListener((ActionEvent e) -> {
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result);
}
}
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. Многопоточность - это сложно, но есть много помощи!