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()
будь-який / усі потоки, які чекали на цей об'єкт. Це все одно, що хлопець з піци подзвонить у двері, тож ви можете сісти і відпочити, чекаючи, замість того, щоб незграбно стояти біля дверей.
Під час виклику очікування / повідомлення про об'єкт ви повинні утримувати блокування синхронізації цього об'єкта, що і робить наведений вище код. Ви можете використовувати будь-який вподобаний об'єкт, якщо обидва потоки використовують один і той же об'єкт: тут я використовував 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
і take
методи BlockingQueue
can throw InterruptedException
s, які перевіряють винятки, з якими потрібно обробляти. У наведеному вище коді, для простоти, винятки відновлено. Можливо, ви віддасте перевагу ловити винятки в методах і повторити спробу виклику путів або прийому, щоб переконатися, що це вдалося. Окрім цього, один момент негарності 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
). Ви також можете використовувати a 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();
});
Таймери
Щоб виконувати періодичні дії, ви можете використовувати a 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 також є a 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. Багатопотоковість складна, але доступна велика допомога!