Синхронизация и управление доступом

Синхронизация позволяет предотвратить одновременный доступ нескольких потоков к общим данным, что устраняет состояния гонки.

В Java для этого используется механизм монитора – внутренней блокировки, привязанной к каждому объекту. Ключевое слово synchronized захватывает монитор объекта, не позволяя другим потокам выполнить код, защищенный тем же монитором.

Методы общения потоков: wait(), notify(), notifyAll() – позволяют потокам координировать действия через монитор:

wait() – отпускает монитор и переводит текущий поток в состояние ожидания до пробуждения.

notify() – пробуждает один из потоков, ожидающих на том же мониторе.

notifyAll() – пробуждает все потоки, ожидающие этот монитор.
<small>Вызывать эти методы можно только внутри блока synchronized на том же самом объекте-мониторе.</small>

Пояснение:
Когда несколько потоков обращаются к одним и тем же объектам или переменным, возникает риск race condition – например, два потока одновременно пытаются увеличить счетчик, в результате некоторые инкременты теряются. Рассмотрим простой пример без синхронизации:
class Counter {
    public int count = 0;
}Counter cnt = new Counter();// Поток А:Thread t1 = new Thread(() -> {
    for(int i=0; i<1000; i++) {
        cnt.count++;  // увеличение счетчика
    }
});// Поток B:Thread t2 = new Thread(() -> {
    for(int i=0; i<1000; i++) {
        cnt.count++;
    }
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Результат: " + cnt.count);
В идеале результат должен быть 2000 (по 1000 инкрементов в каждом потоке). Однако без синхронизации операция cnt.count++ (которая не атомарна, она состоит из чтения, увеличения и записи) может прерываться: потоки вмешиваются друг в друга, и итоговое значение часто оказывается меньше 2000. Чтобы гарантировать корректность, необходима синхронизация доступа к count. Самый простой способ – поместить инкремент в критическую секцию synchronized. В Java каждый объект несет в себе монитор (внутреннюю блокировку). Оператор synchronized перед выполнением кода требует захватить монитор указанного объекта, и освобождает его по выходу из блока. Если один поток уже держит монитор, второй поток, пытающийся войти в этот же синхронизированный блок, будет заблокирован (BLOCKED) до освобождения монитора.
 
Применяя synchronized, решим проблему:
class SyncCounter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}SyncCounter cnt = new SyncCounter();Thread t1 = new Thread(() -> { for(int i=0;i<1000;i++) cnt.increment(); });Thread t2 = new Thread(() -> { for(int i=0;i<1000;i++) cnt.increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(cnt.getCount()); // всегда 2000
Здесь метод increment() объявлен как synchronized. Это эквивалентно блоку synchronized(this) { count++; } – монитором выступает сам объект SyncCounter. Когда поток A выполняет increment(), он захватывает монитор this и тем самым исключает одновременное выполнение increment() во втором потоке до завершения первого вызова. Таким образом, взаимное исключение (mutual exclusion) защищает общие данные. Синхронизировать можно не только метод, но и произвольный блок кода, завязав его на монитор любого объекта: synchronized(someObject) { ... }. Главное, чтобы все потоки синхронизировались на одном и том же объекте. Чаще всего в роли монитора используют либо сам объект с данными (this), либо отдельный объект-заглушку (lock), специально выделенный для этих целей.
 
Монитор в Java реализован на уровне JVM: он следит за тем, чтобы в один момент времени лишь один поток выполнял код внутри блока synchronized на конкретном объекте. Если два потока пытаются выполнить synchronized-блок по одному монитору, то второй будет ждать, пока первый не выйдет. Это предотвращает повреждение общих данных, но может привести к взаимной блокировке (deadlock), если потоки будут ждать друг друга по циклу – при проектировании многопоточных взаимодействий важно избегать подобных ситуаций.
 
Для более сложных сценариев синхронизации Java предоставляет пакет java.util.concurrent.locks. Например, класс ReentrantLock реализует ручную блокировку, похожую на монитор, но с расширенными возможностями: можно пытаться захватить с таймаутом, можно проверять состояние блокировки, использовать несколько условных переменных (Condition) вместо одного механизма wait/notify. В целом, для простой синхронизации достаточно synchronized, а ReentrantLock применяют, когда нужен более тонкий контрольsky.prosky.pro. Например, у ReentrantLock можно вызвать lockInterruptibly() – попытаться захватить, прерываясь, если поток прерван, или tryLock() – не ждать, если лок уже занят, а сразу вернуть результат. С synchronized так делать нельзя – он всегда ждет бесконечно.
 
Теперь об упомянутых методах координации потоков: wait(), notify(), notifyAll(). Они определены в классе Object (потому что привязаны к мониторному механизму любого объекта). Эти методы позволяют одним потокам приостанавливать выполнение, ожидая события, а другим – уведомлять об этих событиях. Классический пример – реализация паттерна "Производитель–Потребитель": один поток производит данные и складывает, другой – потребляет. Когда очередь пуста, потребитель должен ждать, пока производитель что-то не добавит; когда очередь заполнена – производитель ждет, пока потребитель освободит место. Без wait/notify пришлось бы делать активное ожидание (спрашивать в цикле, не появились ли данные), что неэффективно.
 
Метод obj.wait() можно вызвать, только удерживая монитор объекта obj (т.е. внутри блока synchronized(obj)). Вызов wait() освобождает захваченный монитор и приостанавливает поток, добавляя его в очередь ожидания у этого монитораdocs.oracle.com. Поток не будет выполняться, пока другой поток не вызовет obj.notify() или obj.notifyAll() (тоже из синхронизированного блока по тому же объекту). Метод notify() будит один произвольно выбранный поток из ожидающих на мониторе, а notifyAll() – все ожидающие потоки (обычно используется, чтобы никто не остался спать). Однако пробужденные потоки не бегут сразу дальше – чтобы продолжить работу после wait(), поток должен снова захватить тот же монитор. Поэтому если несколько потоков проснулись через notifyAll(), они будут соревноваться за монитор, и только один из них продолжит выполнение сразу, остальные опять заснут, ожидая освобождения монитора.
 
Методы wait/notify довольно низкоуровневые и требуют осторожного использования. 

Основные правила:
1) Вызывать их только внутри synchronized-блока по тому же объекту (иначе будет IllegalMonitorStateException).
2) Всегда проверять условие в цикле при ожидании. Например:

synchronized(queue) {
    while (queue.isEmpty()) {
        queue.wait();
    }
    // очередь не пуста – можно потреблять элемент
    Item item = queue.remove();
}

3) Использование цикла while вместо простого if обусловлено возможными ложными пробуждениями (spurious wakeups) – поток может проснуться от wait() без уведомления, или сразу несколько потребителей проснутся от одного notify. Цикл гарантирует, что поток действительно выйдет из ожидания только когда условие (очередь не пуста) выполнено.
4) Предпочтительно вызывать notify() после того, как обновлено состояние, из-за которого кто-то мог ждать. Например, после добавления элемента в очередь имеет смысл вызвать queue.notify(), находясь в synchronized-блоке, чтобы разбудить одного потребителя. Если вызвать notify() до фактического изменения состояния, проснувшийся поток может тут же проверить условие и снова уйти ждать, хотя казалось бы, уведомление уже произошло.

В современных версиях Java часто нет необходимости вручную использовать wait/notify, потому что есть более высокоуровневые утилиты. Например, вместо собственного списка + wait/notify для производителя/потребителя можно использовать готовую потокобезопасную очередь BlockingQueue из пакета java.util.concurrent – она самостоятельно блокирует потоки при пустой/полной очереди и будит, когда элементы появляютсяjavaguides.net. Также существуют классы CountDownLatch, Semaphore, Exchanger, CyclicBarrier и др., которые реализуют распространенные шаблоны синхронизации (ожидание группы потоков, ограничение доступа к ресурсу, обмен данными между потоками и т.д.). Эти классы облегчают разработку сложных многопоточных взаимодействий, поскольку инкапсулируют логику ожидания и оповещения.
 
Еще одной альтернативой явной синхронизации являются атомарные переменные (AtomicInteger, AtomicReference и др.) и коллекции из пакета java.util.concurrent (например, ConcurrentHashMap, CopyOnWriteArrayList). Они спроектированы для безопасного использования из нескольких потоков без необходимости оборачивать операции в synchronizedjavaguides.net. Под капотом они могут использовать оптимизированные неблокирующие алгоритмы или встроенную низкоуровневую синхронизацию, но для разработчика работа с ними проще и менее ошибкоопасна.
 
Подводя итог: синхронизация – ключевой механизм обеспечения корректности в многопоточных программах Java. На низком уровне она достигается с помощью встроенных мониторов (synchronized и wait/notify), но в реальных приложениях все чаще используют более высокоуровневые конструкции – пулы потоков, синхронизаторы и безопасные для потоков коллекции – которые абстрагируют детали блокировок и делают код более понятным и поддерживаемымjavaguides.net. Грамотное сочетание этих средств позволяет писать многопоточный код, который одновременно эффективно использует ресурсы и остается корректным при параллельном доступе к данным.
 
Автор: к.п.н., Румянцев Сергей Александрович, доцент Финансового университета при Правительстве РФ; доцент ОЧУВО Международного инновационного университета; Консалтинг, управление разработкой ПО; системный и бизнес анализ; менеджмент; аналитиз данных; управление ИТ. Телефон для связи +79269444818 (мессенджеры)   Короткая ссылка:
Синхронизация позволяет предотвратить одновременный доступ нескольких потоков к общим данным, что устраняет состояния гонки.

В Java для этого используется механизм монитора – внутренней блокировки, привязанной к каждому объекту. Ключевое слово synchronized захватывает монитор объекта, не позволяя другим потокам выполнить код, защищенный тем же монитором.

Методы общения потоков: wait(), notify(), notifyAll() – позволяют потокам координировать действия через монитор:

wait() – отпускает монитор и переводит текущий поток в состояние ожидания до пробуждения.

notify() – пробуждает один из потоков, ожидающих на том же мониторе.

notifyAll() – пробуждает все потоки, ожидающие этот монитор.
<small>Вызывать эти методы можно только внутри блока synchronized на том же самом объекте-мониторе.</small>

Пояснение:
Когда несколько потоков обращаются к одним и тем же объектам или переменным, возникает риск race condition – например, два потока одновременно пытаются увеличить счетчик, в результате некоторые инкременты теряются. Рассмотрим простой пример без синхронизации:
class Counter {
    public int count = 0;
}Counter cnt = new Counter();// Поток А:Thread t1 = new Thread(() -> {
    for(int i=0; i<1000; i++) {
        cnt.count++;  // увеличение счетчика
    }
});// Поток B:Thread t2 = new Thread(() -> {
    for(int i=0; i<1000; i++) {
        cnt.count++;
    }
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Результат: " + cnt.count);
В идеале результат должен быть 2000 (по 1000 инкрементов в каждом потоке). Однако без синхронизации операция cnt.count++ (которая не атомарна, она состоит из чтения, увеличения и записи) может прерываться: потоки вмешиваются друг в друга, и итоговое значение часто оказывается меньше 2000. Чтобы гарантировать корректность, необходима синхронизация доступа к count. Самый простой способ – поместить инкремент в критическую секцию synchronized. В Java каждый объект несет в себе монитор (внутреннюю блокировку). Оператор synchronized перед выполнением кода требует захватить монитор указанного объекта, и освобождает его по выходу из блока. Если один поток уже держит монитор, второй поток, пытающийся войти в этот же синхронизированный блок, будет заблокирован (BLOCKED) до освобождения монитора.
 
Применяя synchronized, решим проблему:
class SyncCounter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}SyncCounter cnt = new SyncCounter();Thread t1 = new Thread(() -> { for(int i=0;i<1000;i++) cnt.increment(); });Thread t2 = new Thread(() -> { for(int i=0;i<1000;i++) cnt.increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(cnt.getCount()); // всегда 2000
Здесь метод increment() объявлен как synchronized. Это эквивалентно блоку synchronized(this) { count++; } – монитором выступает сам объект SyncCounter. Когда поток A выполняет increment(), он захватывает монитор this и тем самым исключает одновременное выполнение increment() во втором потоке до завершения первого вызова. Таким образом, взаимное исключение (mutual exclusion) защищает общие данные. Синхронизировать можно не только метод, но и произвольный блок кода, завязав его на монитор любого объекта: synchronized(someObject) { ... }. Главное, чтобы все потоки синхронизировались на одном и том же объекте. Чаще всего в роли монитора используют либо сам объект с данными (this), либо отдельный объект-заглушку (lock), специально выделенный для этих целей.
 
Монитор в Java реализован на уровне JVM: он следит за тем, чтобы в один момент времени лишь один поток выполнял код внутри блока synchronized на конкретном объекте. Если два потока пытаются выполнить synchronized-блок по одному монитору, то второй будет ждать, пока первый не выйдет. Это предотвращает повреждение общих данных, но может привести к взаимной блокировке (deadlock), если потоки будут ждать друг друга по циклу – при проектировании многопоточных взаимодействий важно избегать подобных ситуаций.
 
Для более сложных сценариев синхронизации Java предоставляет пакет java.util.concurrent.locks. Например, класс ReentrantLock реализует ручную блокировку, похожую на монитор, но с расширенными возможностями: можно пытаться захватить с таймаутом, можно проверять состояние блокировки, использовать несколько условных переменных (Condition) вместо одного механизма wait/notify. В целом, для простой синхронизации достаточно synchronized, а ReentrantLock применяют, когда нужен более тонкий контрольsky.prosky.pro. Например, у ReentrantLock можно вызвать lockInterruptibly() – попытаться захватить, прерываясь, если поток прерван, или tryLock() – не ждать, если лок уже занят, а сразу вернуть результат. С synchronized так делать нельзя – он всегда ждет бесконечно.
 
Теперь об упомянутых методах координации потоков: wait(), notify(), notifyAll(). Они определены в классе Object (потому что привязаны к мониторному механизму любого объекта). Эти методы позволяют одним потокам приостанавливать выполнение, ожидая события, а другим – уведомлять об этих событиях. Классический пример – реализация паттерна "Производитель–Потребитель": один поток производит данные и складывает, другой – потребляет. Когда очередь пуста, потребитель должен ждать, пока производитель что-то не добавит; когда очередь заполнена – производитель ждет, пока потребитель освободит место. Без wait/notify пришлось бы делать активное ожидание (спрашивать в цикле, не появились ли данные), что неэффективно.
 
Метод obj.wait() можно вызвать, только удерживая монитор объекта obj (т.е. внутри блока synchronized(obj)). Вызов wait() освобождает захваченный монитор и приостанавливает поток, добавляя его в очередь ожидания у этого монитораdocs.oracle.com. Поток не будет выполняться, пока другой поток не вызовет obj.notify() или obj.notifyAll() (тоже из синхронизированного блока по тому же объекту). Метод notify() будит один произвольно выбранный поток из ожидающих на мониторе, а notifyAll() – все ожидающие потоки (обычно используется, чтобы никто не остался спать). Однако пробужденные потоки не бегут сразу дальше – чтобы продолжить работу после wait(), поток должен снова захватить тот же монитор. Поэтому если несколько потоков проснулись через notifyAll(), они будут соревноваться за монитор, и только один из них продолжит выполнение сразу, остальные опять заснут, ожидая освобождения монитора.
 
Методы wait/notify довольно низкоуровневые и требуют осторожного использования. 

Основные правила:
1) Вызывать их только внутри synchronized-блока по тому же объекту (иначе будет IllegalMonitorStateException).
2) Всегда проверять условие в цикле при ожидании. Например:

synchronized(queue) {
    while (queue.isEmpty()) {
        queue.wait();
    }
    // очередь не пуста – можно потреблять элемент
    Item item = queue.remove();
}

3) Использование цикла while вместо простого if обусловлено возможными ложными пробуждениями (spurious wakeups) – поток может проснуться от wait() без уведомления, или сразу несколько потребителей проснутся от одного notify. Цикл гарантирует, что поток действительно выйдет из ожидания только когда условие (очередь не пуста) выполнено.
4) Предпочтительно вызывать notify() после того, как обновлено состояние, из-за которого кто-то мог ждать. Например, после добавления элемента в очередь имеет смысл вызвать queue.notify(), находясь в synchronized-блоке, чтобы разбудить одного потребителя. Если вызвать notify() до фактического изменения состояния, проснувшийся поток может тут же проверить условие и снова уйти ждать, хотя казалось бы, уведомление уже произошло.

В современных версиях Java часто нет необходимости вручную использовать wait/notify, потому что есть более высокоуровневые утилиты. Например, вместо собственного списка + wait/notify для производителя/потребителя можно использовать готовую потокобезопасную очередь BlockingQueue из пакета java.util.concurrent – она самостоятельно блокирует потоки при пустой/полной очереди и будит, когда элементы появляютсяjavaguides.net. Также существуют классы CountDownLatch, Semaphore, Exchanger, CyclicBarrier и др., которые реализуют распространенные шаблоны синхронизации (ожидание группы потоков, ограничение доступа к ресурсу, обмен данными между потоками и т.д.). Эти классы облегчают разработку сложных многопоточных взаимодействий, поскольку инкапсулируют логику ожидания и оповещения.
 
Еще одной альтернативой явной синхронизации являются атомарные переменные (AtomicInteger, AtomicReference и др.) и коллекции из пакета java.util.concurrent (например, ConcurrentHashMap, CopyOnWriteArrayList). Они спроектированы для безопасного использования из нескольких потоков без необходимости оборачивать операции в synchronizedjavaguides.net. Под капотом они могут использовать оптимизированные неблокирующие алгоритмы или встроенную низкоуровневую синхронизацию, но для разработчика работа с ними проще и менее ошибкоопасна.
 
Подводя итог: синхронизация – ключевой механизм обеспечения корректности в многопоточных программах Java. На низком уровне она достигается с помощью встроенных мониторов (synchronized и wait/notify), но в реальных приложениях все чаще используют более высокоуровневые конструкции – пулы потоков, синхронизаторы и безопасные для потоков коллекции – которые абстрагируют детали блокировок и делают код более понятным и поддерживаемымjavaguides.net. Грамотное сочетание этих средств позволяет писать многопоточный код, который одновременно эффективно использует ресурсы и остается корректным при параллельном доступе к данным.
 
https://webprogr.ru/~j1TTy
Короткая ссылка на новость:https://webprogr.ru/~j1TTy


Последние новости

Синхронизация и управление доступом

Синхронизация позволяет предотвратить одновременный доступ нескольких потоков к общим данным, что устраняет состояния гонки.

В Java для этого используется механизм монитора – внутренней блокировки, привязанной к каждому объекту. Ключевое слово synchronized захватывает монитор объекта, не позволяя другим потокам выполнить код, защищенный тем же монитором.

Методы общения потоков: wait(), notify(), notifyAll() – позволяют потокам координировать действия через монитор:

wait() – отпускает монитор и переводит текущий поток в состояние ожидания до пробуждения.

notify() – пробуждает один из потоков, ожидающих на том же мониторе.

notifyAll() – пробуждает все потоки, ожидающие этот монитор.
<small>Вызывать эти методы можно только внутри блока synchronized на том же самом объекте-мониторе.</small>

Пояснение:
Когда несколько потоков обращаются к одним и тем же объектам или переменным, возникает риск race condition – например, два потока одновременно пытаются увеличить счетчик, в результате некоторые инкременты теряются. Рассмотрим простой пример без синхронизации:
class Counter {
    public int count = 0;
}Counter cnt = new Counter();// Поток А:Thread t1 = new Thread(() -> {
    for(int i=0; i<1000; i++) {
        cnt.count++;  // увеличение счетчика
    }
});// Поток B:Thread t2 = new Thread(() -> {
    for(int i=0; i<1000; i++) {
        cnt.count++;
    }
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Результат: " + cnt.count);
В идеале результат должен быть 2000 (по 1000 инкрементов в каждом потоке). Однако без синхронизации операция cnt.count++ (которая не атомарна, она состоит из чтения, увеличения и записи) может прерываться: потоки вмешиваются друг в друга, и итоговое значение часто оказывается меньше 2000. Чтобы гарантировать корректность, необходима синхронизация доступа к count. Самый простой способ – поместить инкремент в критическую секцию synchronized. В Java каждый объект несет в себе монитор (внутреннюю блокировку). Оператор synchronized перед выполнением кода требует захватить монитор указанного объекта, и освобождает его по выходу из блока. Если один поток уже держит монитор, второй поток, пытающийся войти в этот же синхронизированный блок, будет заблокирован (BLOCKED) до освобождения монитора.
 
Применяя synchronized, решим проблему:
class SyncCounter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}SyncCounter cnt = new SyncCounter();Thread t1 = new Thread(() -> { for(int i=0;i<1000;i++) cnt.increment(); });Thread t2 = new Thread(() -> { for(int i=0;i<1000;i++) cnt.increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(cnt.getCount()); // всегда 2000
Здесь метод increment() объявлен как synchronized. Это эквивалентно блоку synchronized(this) { count++; } – монитором выступает сам объект SyncCounter. Когда поток A выполняет increment(), он захватывает монитор this и тем самым исключает одновременное выполнение increment() во втором потоке до завершения первого вызова. Таким образом, взаимное исключение (mutual exclusion) защищает общие данные. Синхронизировать можно не только метод, но и произвольный блок кода, завязав его на монитор любого объекта: synchronized(someObject) { ... }. Главное, чтобы все потоки синхронизировались на одном и том же объекте. Чаще всего в роли монитора используют либо сам объект с данными (this), либо отдельный объект-заглушку (lock), специально выделенный для этих целей.
 
Монитор в Java реализован на уровне JVM: он следит за тем, чтобы в один момент времени лишь один поток выполнял код внутри блока synchronized на конкретном объекте. Если два потока пытаются выполнить synchronized-блок по одному монитору, то второй будет ждать, пока первый не выйдет. Это предотвращает повреждение общих данных, но может привести к взаимной блокировке (deadlock), если потоки будут ждать друг друга по циклу – при проектировании многопоточных взаимодействий важно избегать подобных ситуаций.
 
Для более сложных сценариев синхронизации Java предоставляет пакет java.util.concurrent.locks. Например, класс ReentrantLock реализует ручную блокировку, похожую на монитор, но с расширенными возможностями: можно пытаться захватить с таймаутом, можно проверять состояние блокировки, использовать несколько условных переменных (Condition) вместо одного механизма wait/notify. В целом, для простой синхронизации достаточно synchronized, а ReentrantLock применяют, когда нужен более тонкий контрольsky.prosky.pro. Например, у ReentrantLock можно вызвать lockInterruptibly() – попытаться захватить, прерываясь, если поток прерван, или tryLock() – не ждать, если лок уже занят, а сразу вернуть результат. С synchronized так делать нельзя – он всегда ждет бесконечно.
 
Теперь об упомянутых методах координации потоков: wait(), notify(), notifyAll(). Они определены в классе Object (потому что привязаны к мониторному механизму любого объекта). Эти методы позволяют одним потокам приостанавливать выполнение, ожидая события, а другим – уведомлять об этих событиях. Классический пример – реализация паттерна "Производитель–Потребитель": один поток производит данные и складывает, другой – потребляет. Когда очередь пуста, потребитель должен ждать, пока производитель что-то не добавит; когда очередь заполнена – производитель ждет, пока потребитель освободит место. Без wait/notify пришлось бы делать активное ожидание (спрашивать в цикле, не появились ли данные), что неэффективно.
 
Метод obj.wait() можно вызвать, только удерживая монитор объекта obj (т.е. внутри блока synchronized(obj)). Вызов wait() освобождает захваченный монитор и приостанавливает поток, добавляя его в очередь ожидания у этого монитораdocs.oracle.com. Поток не будет выполняться, пока другой поток не вызовет obj.notify() или obj.notifyAll() (тоже из синхронизированного блока по тому же объекту). Метод notify() будит один произвольно выбранный поток из ожидающих на мониторе, а notifyAll() – все ожидающие потоки (обычно используется, чтобы никто не остался спать). Однако пробужденные потоки не бегут сразу дальше – чтобы продолжить работу после wait(), поток должен снова захватить тот же монитор. Поэтому если несколько потоков проснулись через notifyAll(), они будут соревноваться за монитор, и только один из них продолжит выполнение сразу, остальные опять заснут, ожидая освобождения монитора.
 
Методы wait/notify довольно низкоуровневые и требуют осторожного использования. 

Основные правила:
1) Вызывать их только внутри synchronized-блока по тому же объекту (иначе будет IllegalMonitorStateException).
2) Всегда проверять условие в цикле при ожидании. Например:

synchronized(queue) {
    while (queue.isEmpty()) {
        queue.wait();
    }
    // очередь не пуста – можно потреблять элемент
    Item item = queue.remove();
}

3) Использование цикла while вместо простого if обусловлено возможными ложными пробуждениями (spurious wakeups) – поток может проснуться от wait() без уведомления, или сразу несколько потребителей проснутся от одного notify. Цикл гарантирует, что поток действительно выйдет из ожидания только когда условие (очередь не пуста) выполнено.
4) Предпочтительно вызывать notify() после того, как обновлено состояние, из-за которого кто-то мог ждать. Например, после добавления элемента в очередь имеет смысл вызвать queue.notify(), находясь в synchronized-блоке, чтобы разбудить одного потребителя. Если вызвать notify() до фактического изменения состояния, проснувшийся поток может тут же проверить условие и снова уйти ждать, хотя казалось бы, уведомление уже произошло.

В современных версиях Java часто нет необходимости вручную использовать wait/notify, потому что есть более высокоуровневые утилиты. Например, вместо собственного списка + wait/notify для производителя/потребителя можно использовать готовую потокобезопасную очередь BlockingQueue из пакета java.util.concurrent – она самостоятельно блокирует потоки при пустой/полной очереди и будит, когда элементы появляютсяjavaguides.net. Также существуют классы CountDownLatch, Semaphore, Exchanger, CyclicBarrier и др., которые реализуют распространенные шаблоны синхронизации (ожидание группы потоков, ограничение доступа к ресурсу, обмен данными между потоками и т.д.). Эти классы облегчают разработку сложных многопоточных взаимодействий, поскольку инкапсулируют логику ожидания и оповещения.
 
Еще одной альтернативой явной синхронизации являются атомарные переменные (AtomicInteger, AtomicReference и др.) и коллекции из пакета java.util.concurrent (например, ConcurrentHashMap, CopyOnWriteArrayList). Они спроектированы для безопасного использования из нескольких потоков без необходимости оборачивать операции в synchronizedjavaguides.net. Под капотом они могут использовать оптимизированные неблокирующие алгоритмы или встроенную низкоуровневую синхронизацию, но для разработчика работа с ними проще и менее ошибкоопасна.
 
Подводя итог: синхронизация – ключевой механизм обеспечения корректности в многопоточных программах Java. На низком уровне она достигается с помощью встроенных мониторов (synchronized и wait/notify), но в реальных приложениях все чаще используют более высокоуровневые конструкции – пулы потоков, синхронизаторы и безопасные для потоков коллекции – которые абстрагируют детали блокировок и делают код более понятным и поддерживаемымjavaguides.net. Грамотное сочетание этих средств позволяет писать многопоточный код, который одновременно эффективно использует ресурсы и остается корректным при параллельном доступе к данным.
 
Рейтинг@Mail.ru