Взаимодействие между потоками в Java. Wait и notify. | Блог еще одного разработчика
Отличная статья на русском языке.
К сожалению, исходная статья более не доступна по ссылке.
Привожу копию из архивов с ресурса archive-ru.com.
Исходная статья с сайта http://stremoukhov.ru.
Отличная статья на русском языке.
К сожалению, исходная статья более не доступна по ссылке.
Привожу копию из архивов с ресурса archive-ru.com.
Исходная статья с сайта http://stremoukhov.ru.
Взаимодействие между потоками в Java. Wait и notify.
Представим ситуацию, что у нас многопотоковое приложение, написанное на Java. Есть некий класс, выполняющий какое-либо конкретное действие с данными, например, отправку их по почте, а сами данные подготавливаются в другом месте кода в другом потоке. Перед отправкой данных нам необходимо как-то связаться с потоком, подготавливающим данные, дабы поймать момент, когда они будут готовы.
Пример 1. Простой, но неправильный пример
public class DataManager {
private static boolean ready = false;
public void sendData() {
while (!ready) {
// waiting
System.out.println("Waiting for data...");
}
// continue execution and sending data
System.out.println("Sending data...");
}
public void prepareData() {
System.out.println("Data prepared");
ready = true;
}
}
private static boolean ready = false;
public void sendData() {
while (!ready) {
// waiting
System.out.println("Waiting for data...");
}
// continue execution and sending data
System.out.println("Sending data...");
}
public void prepareData() {
System.out.println("Data prepared");
ready = true;
}
}
Допустим, что первый поток должен готовить данные, а второй отправлять. Из данного примера видно, что отправляющий поток будет ждать данные в цикле. Это, конечно, будет работать, особенно, если время ожидание мизерное. Но по сути, ожидая данных поток серьезно загрузит процессор, отнимая драгоценные ресурсы на подготовку данных. Правильнее будет каким-то образом приостановить второй поток, пока первый не подготовит данные.
Пример 2. Ожидание с помощью wait() и notifyAll()
public class DataManager {
private static final Object monitor = new Object();
public void sendData() {
synchronized (monitor) {
System.out.println("Waiting for data...");
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// continue execution and sending data
System.out.println("Sending data...");
}
}
public void prepareData() {
synchronized (monitor) {
System.out.println("Data prepared");
monitor.notifyAll();
}
}
}
private static final Object monitor = new Object();
public void sendData() {
synchronized (monitor) {
System.out.println("Waiting for data...");
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// continue execution and sending data
System.out.println("Sending data...");
}
}
public void prepareData() {
synchronized (monitor) {
System.out.println("Data prepared");
monitor.notifyAll();
}
}
}
Как видно из данного примера, отправляющий данные поток не циклиться, а вызывает метод wait() у нашего объекта-монитора. Напомню принцип работы синхронизации: как только первый поток попадает в блок synchronized, он становится владельцем объекта monitor и "блокирует" его. Никакой другой поток не сможет попасть в блок, т.е. стать владельцем этого объекта до тех пор, пока поток владелец не "отпустит" монитор, выйдя из блока синхронизации.
Важно! Методы wait(), notify() и notifyAll() должны обязательно находиться внутри блока synchronized, либо внутри synchronized-метода, иначе вы получите Exception. Но возникает вопрос, каким образом второй поток сможет попасть в блок синхронизации, чтобы вызвать notifyAll(), если первый в этот момент будет владельцем monitor'а и будет ждать notify() ? Все просто - как только поток достигает метода wait() он перестает быть владельцем монитора, блокировка снимается, а поток уходит в сон.
Потенциальные проблемы данного кода:
1. Возможна ситуация, когда первый поток подготовил данные раньше, чем второй начал ждать его. В этом случае notifyAll() никого не разбудит и соответственно, когда второй поток дойдет до wait(), процесс остановится навсегда.
2. Существуют в природе так называемые "ложные пробуждения", когда поток просыпается сам по себе, без всяких на то причин.
1. Возможна ситуация, когда первый поток подготовил данные раньше, чем второй начал ждать его. В этом случае notifyAll() никого не разбудит и соответственно, когда второй поток дойдет до wait(), процесс остановится навсегда.
2. Существуют в природе так называемые "ложные пробуждения", когда поток просыпается сам по себе, без всяких на то причин.
Чтобы избежать обе проблемы, усовершенствуем наш код.
Пример 3. Ожидание в цикле.
Пример 3. Ожидание в цикле.
public class DataManager implements Runnable {
private static final Object monitor = new Object();
private static boolean ready = false;
public void prepareData() {
synchronized (monitor) {
System.out.println("Data prepared");
ready = true;
monitor.notifyAll();
}
}
public void sendData() {
synchronized (monitor) {
System.out.println("Waiting for data...");
while (!ready) {
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// continue execution and sending data
System.out.println("Sending data...");
}
}
}
private static final Object monitor = new Object();
private static boolean ready = false;
public void prepareData() {
synchronized (monitor) {
System.out.println("Data prepared");
ready = true;
monitor.notifyAll();
}
}
public void sendData() {
synchronized (monitor) {
System.out.println("Waiting for data...");
while (!ready) {
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// continue execution and sending data
System.out.println("Sending data...");
}
}
}
Не смотря на цикл как в примере 1, здесь у нас не будет расхода ресурсов процессора, т.к. цикл повторится только в случае ложного пробуждения, или прерывания. Также, мы не попадем в цикл, а следовательно на метод wait() если данные уже готовы на момент исполнения sendData();