Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Лекции 2025. Java. Белая / Ответы на билеты. Java

.pdf
Скачиваний:
0
Добавлен:
02.01.2026
Размер:
4.52 Mб
Скачать

:

1. Действие:

Пробуждает один произвольный поток, который ожидает на мониторе этого объекта (т.е. ранее вызвал на этом объекте).

Если нет потоков, ожидающих на этом мониторе, вызов не имеет никакого эффекта.

2.Выбор потока: Нельзя предсказать, какой именно из ожидающих потоков будет пробужден, если их несколько.

3.Освобождение монитора: Важно понимать, что поток, вызывающий , не освобождает монитор немедленно. Он продолжает владеть монитором до тех

пор, пока не выйдет из блока или метода. Только после этого пробужденный поток (и другие потоки) сможет попытаться захватить монитор.

:

1. Действие:

Пробуждает все потоки, которые ожидают на мониторе этого объекта. Если нет ожидающих потоков, вызов не имеет эффекта.

2.Конкуренция за монитор: Все пробужденные потоки переходят в состояние

и начинают конкурировать за захват монитора. Только один из них в итоге его получит и продолжит выполнение.

3.Освобождение монитора: Аналогично , монитор освобождается только после выхода вызывающего потока из секции.

Пример (классическая задача "Производитель-Потребитель"):

В этом примере используется для ожидания, пока буфер не перестанет быть полным (для производителя) или пустым (для потребителя). используется для пробуждения всех ожидающих потоков противоположного типа, когда состояние буфера изменилось. Цикл вокруг гарантирует, что условие перепроверяется после пробуждения.

67. В чем разница между notify() и notifyAll()?

Разница между и заключается в том, сколько ожидающих

потоков они пробуждают:

:

Пробуждает один (случайный) поток: Если на мониторе объекта ожидают несколько потоков (вызвавших на этом объекте), выберет и пробудит только один из них.

Непредсказуемость: Нет гарантии, какой именно из ожидающих потоков будет пробужден. Выбор зависит от реализации JVM и может быть непредсказуемым.

Когда использовать (с осторожностью):

Когда вы уверены, что все ожидающие потоки однотипны (ждут одного и того же условия, и любой из них может корректно продолжить работу).

Когда пробуждение любого одного потока достаточно для дальнейшего прогресса системы (например, если есть только один тип потребителей, и любой из них может обработать ресурс).

В сценариях с высокой производительностью, где пробуждение лишних потоков (которые все равно не смогут продолжить работу и снова заснут)

может быть накладно. Однако это требует очень тщательного анализа,

чтобы избежать проблем.

Потенциальные проблемы:

Пропущенный сигнал (Missed Signal / Lost Wake-up) или "потерянное пробуждение": Если пробужденный поток не может выполнить условие и снова засыпает, а другие потоки, которые могли бы выполнить условие, не были пробуждены, система может "зависнуть".

Несправедливость: Некоторые потоки могут ожидать дольше других, если они постоянно не выбираются методом .

:

Пробуждает все потоки: Пробуждает все потоки, которые ожидают на мониторе данного объекта.

Безопасность и надежность: Это, как правило, более безопасный и надежный

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

Когда использовать (чаще всего рекомендуется):

Когда несколько потоков могут ожидать разных условий, и вы не знаете, какой из них сможет продолжить работу.

Когда вы хотите гарантировать, что если условие стало истинным для какоголибо потока, он будет пробужден.

В большинстве случаев, если вы не уверены, какой метод использовать,

является более предпочтительным выбором для избежания тонких ошибок синхронизации.

Потенциальные проблемы:

"Громоподобное пробуждение" (Thundering Herd Problem): Если пробуждается большое количество потоков, и все они начинают конкурировать

за один и тот же монитор (и, возможно, другие ресурсы), это может создать всплеск нагрузки и снизить производительность. Однако, для большинства приложений на Java это не является серьезной проблемой, и польза от

надежности

часто перевешивает.

 

Сравнение:

 

 

 

 

 

Характеристика

 

 

 

 

 

Кого пробуждает?

Один (случайный)

Все ожидающие потоки.

 

ожидающий поток.

 

 

 

 

Предсказуемость

Низкая (какой поток будет

Высокая (все будут

 

пробужден).

пробуждены).

 

 

 

Безопасность

Менее безопасен, может

Более безопасен и надежен.

 

привести к "потерянным

 

 

пробуждениям", если

 

 

используется неправильно.

 

 

 

 

Производительность

Может быть немного

Может вызвать

 

быстрее в некоторых

"громоподобное

 

специфических сценариях

пробуждение", если много

 

(меньше переключений

потоков.

 

контекста).

 

 

 

 

Рекомендация

Использовать с большой

Обычно предпочтительнее

 

осторожностью и только при

для большинства

 

полном понимании

сценариев, особенно если

 

последствий.

есть сомнения.

 

 

 

Важное замечание о в цикле :

Независимо от того, используете ли вы или , крайне важно,

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

Это необходимо, потому что:

1.Ложные пробуждения (Spurious Wakeups): Поток может быть пробужден, даже если или не вызывались (хотя это редко).

2.Условие могло измениться: Между моментом вызова / и

моментом, когда пробужденный поток фактически снова захватит монитор и

продолжит выполнение, другой поток мог изменить условие обратно.

3.Разные условия для разных потоков: Если используется , и потоки ждут разных условий, каждый должен перепроверить свое условие.

Вывод:

является более робастным и менее подверженным ошибкам выбором в большинстве ситуаций. следует использовать только тогда, когда вы точно знаете, что это безопасно и оправдано с точки зрения производительности в вашем конкретном случае (что требует глубокого понимания логики синхронизации).

68. Почему методы wait() и notify() вызываются только в синхронизированном блоке?

Методы , и (которые принадлежат классу )

должны вызываться только из синхронизированного контекста (то есть из метода или блока) по нескольким фундаментальным

причинам, связанным с корректной работой механизма мониторов и предотвращением состояний гонки:

1. Требование владения монитором (Intrinsic Lock Ownership):

Эти методы оперируют состоянием ожидания/уведомления, которое

неразрывно связано с монитором объекта.

Чтобы вызвать , или , текущий поток должен владеть монитором объекта .

Единственный способ для потока гарантированно владеть монитором объекта в Java — это войти в метод этого объекта или в блок, использующий этот объект в качестве блокировки.

Если поток пытается вызвать эти методы, не владея монитором, будет выброшено исключение . Это защитный механизм, предотвращающий некорректное использование.

2. Предотвращение состояний гонки (Race Conditions) с условием ожидания:

Представьте, что поток A хочет дождаться, пока условие станет истинным.

Без :

1.Поток A проверяет условие . Оно ложно.

2.В этот момент планировщик переключает контекст на поток B.

3.Поток B изменяет данные так, что условие становится истинным, и вызывает (или ).

4.Планировщик переключает контекст обратно на поток A.

5.Поток A, не зная об изменении, вызывает , так как он "думает", что условие все еще ложно.

В результате поток A засыпает, а сигнал от потока B был

"потерян" для потока A, так как он был отправлен до того, как A вошел в состояние ожидания. Поток A может остаться в состоянии ожидания навсегда (если не будет других ).

С :

1.Поток A входит в блок, захватывая монитор.

2.Поток A проверяет условие . Оно ложно.

3.Поток A вызывает . При этом он атомарно (как единая, неделимая операция) освобождает монитор и переходит в состояние ожидания.

4.Теперь поток B может захватить монитор (так как он был освобожден потоком A).

5.Поток B изменяет данные так, что условие становится истинным, и вызывает (или ).

6.Поток B выходит из блока, освобождая монитор.

7.Поток A пробуждается, пытается снова захватить монитор. После захвата

он выходит из и снова проверяет условие в цикле .

Если оно истинно, он продолжает работу.

Синхронизация гарантирует, что проверка условия и переход в состояние ожидания () происходят атомарно относительно изменений этого условия и уведомлений () другими потоками.

3. Обеспечение видимости изменений (Memory Visibility):

Вход и выход из блока устанавливают отношения "happensbefore".

Когда поток A выходит из блока (после вызова или

), все изменения, сделанные им в разделяемых переменных до этого момента, становятся видимыми для потока B, который затем входит в блок (после пробуждения из ).

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

4. Предотвращение "потерянных пробуждений" (Lost Wake-ups):

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

Что происходит, если вызвать / вне блока?

JVM выбросит во время выполнения. Это происходит потому, что для выполнения этих операций поток должен быть текущим владельцем монитора объекта, на котором вызываются эти методы.

Пример:

В этом примере:

захватывает , проверяет , и если оно , вызывает .

 

 

захватывает тот же

, устанавливает

в

и вызывает

 

.

 

Без

 

вызов

или

привел бы к

 

 

.

 

 

69. Чем отличается работа метода wait() с параметром и без параметра?

Метод в классе имеет три перегруженные версии:

1. (без параметров)

2. (с одним параметром - таймаут в миллисекундах)

3.

(с двумя параметрами - таймаут в миллисекундах и дополнительная точность в наносекундах)

Основное отличие между версией без параметров и версиями с параметрами заключается в условии пробуждения потока, связанном со временем.

(без параметров):

Поведение: Заставляет текущий поток (который должен владеть монитором объекта) освободить монитор и перейти в состояние .

Условия пробуждения: Поток будет ожидать неопределенно долго, пока не произойдет одно из следующих событий:

1.Другой поток вызовет метод или на том же объектемониторе.

2.Поток будет прерван () другим потоком (что вызовет

).

3.Произойдет "ложное пробуждение" (spurious wakeup) - редкое явление, но возможное.

Использование: Когда поток должен ждать выполнения условия, и нет необходимости ограничивать время ожидания. Поток полностью полагается на внешние уведомления.

и (с параметрами):

Поведение: Заставляет текущий поток (который должен владеть монитором объекта) освободить монитор и перейти в состояние .

Параметры:

: Максимальное время ожидания в миллисекундах.

: Дополнительное время ожидания в наносекундах (от 0 до 999999). Если , то увеличивается на 1, если больше или равно 500000 наносекунд (0.5 миллисекунды), в соответствии со спецификацией. Фактическая точность ожидания зависит от системного таймера.

Условия пробуждения: Поток будет ожидать до тех пор, пока не произойдет одно из следующих событий, что бы ни случилось раньше:

1.Другой поток вызовет метод или на том же объектемониторе.

2.Истечет указанный , если указаны).

3.Поток будет прерван () другим потоком (что вызовет

).

4. Произойдет "ложное пробуждение".

Возврат из :

Если поток пробужден из-за / или прерывания до истечения таймаута, он ведет себя так же, как и при без параметров (пытается снова захватить монитор).

Если поток пробужден по истечении таймаута (и не было других причин пробуждения), он также пытается снова захватить монитор. Важно: после пробуждения по таймауту, условие, которого ждал поток, может все еще не быть выполненным. Поэтому необходимо всегда перепроверять условие в

цикле , даже если использовался с таймаутом.