"Случайный" выход из Object.wait()
Как хорошо всем изветно методы класса Object wait и notify служат для поддержки многопоточности
языка Java и техника их использования также знакома всем, но что кажется несколько менее известным
это случаи, при которых ждущий поток прерывает выполнение метода wait.
Вот вольный перевод части спецификации для метода wait(long timeout):
«После вызова метода wait() текущий поток (назовём его Т) помещает себя в очередь ожидания данного объекта, при этом поток снимает блокировку с данного объекта. После этого поток Т останавливается и игнорируется планировщиком потоков пока не случится одно из четырёх событий:
- другой поток вызывает notify() для этого объекта и поток Т был произовольным образом выбран для пробуждения из множества всех ожидающих потоков
- другой поток вызывает notifyAll() для этого объекта
- другой поток прерывает поток Т (с помощью метода interrupt)
- прошёл указанный таймаут(если таймаут равен 0, такой таймаут не учитывается и поток Т должен ждать уведомления)»
А вот что сказано ниже, двумя абзацами ниже:
«Также поток может быть разблокирован без уведомления, прерывания или истечении таймаута (так называемое случайное пробуждение, (spurious wakeup)). Поскольку это может случаться на практике, приложение должно защищать себя от этого, проверяя условие которое определяет, должен поток быть пробуждён или должен продолжать ждать. Другими словами метод wait должен вызываться в цикле вида:
synchronized (obj) {
while (<уловие не выполнено>)
obj.wait(timeout);
... // выполнить действия соответствующие условию
}
»
Честно говоря пока я не столкнулся на практике с этим последним случаем выхода из wait я не знал о нём, этот случай не был должным образом отмечен в литературе, которой я пользовался, а упоминание об этом в спецификации не бросается в глаза пока не начнёшь внимательно вчитываться.
Чтобы все могли проникнуться необходимостью использования wait иключительно в цикле и с непременной проверкой условия окончания ожидания, приведу программу, которая демонстрирует возможность случайного выхода из wait при использовании JDK6 Beta2. Этот код не имеет никакого смысла кроме того, что на практике была обнаружена высокая вероятность случайного выхода именно в такой ситуации:
//SpuriousExitFromWait.java
class WaitingThread
extends Thread
{
public void run()
{
try
{
sleep(Long.MAX_VALUE);
}
catch(InterruptedException e)
{
// главный поток должен прервать WaitingThread
}
synchronized(this)
{
try
{
wait(0);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
// этот код выполниться только в случае 'случайного' выхода из wait
System.out.println("Поток " + currentThread().getName() + " вышел из wait()");
}
}
public class SpuriousExitFromWait
{
public static void main(String[] args)
{
// запускаем 50 потоков чтобы увеличить вероятность того что случится случайный выход из wait
WaitingThread threads[] = new WaitingThread[50];
for(int i = 0; i < threads.length; i++)
{
threads[i] = new WaitingThread();
threads[i].start();
// ждём пока поток не вызовет sleep
while(threads[i].getState() != Thread.State.TIMED_WAITING)
Thread.yield();
threads[i].interrupt();
}
try
{
// ждём 1 секунду
Thread.sleep(1000);
}
catch(InterruptedException e)
{
}
// выходим чтобы не ждать завершения всех потоков
System.exit(0);
}
}
Эта программа запускалась с использование JDK6 Beta2 на следующих платформах: Windows-i586, Linux-i586, Solaris SPARC, Solaris-i586.
На Linux-i586 и Windows-i586 иногда (не в 100% случаев) один или несколько WaitingThread выходят из wait, хотя они не были прерваны и метод notify тоже не вызывался, в этом случае программа выводит, например, следующее:
Поток Thread-47 вышел из wait()
На Solaris SPARC и Solaris-i586 подобного не происходит.
Мы можем убедиться на практике, что предупреждение спецификации не стоит игнорировать поскольку подобную проблему очень сложно обнаружить и поэтому лучше её избегать.
Также стоит отметить, что проверка условия окончания ожидания должна выполняться с учётом особенностей модели памяти Java, это, например, значит, что если в качестве этого условия используется переменная типа boolean, эта переменная должна быть объявлена с использованием модификатора volatile:
class WaitingThread
{
// условие выхода из wait
private volatile boolean endWait;
public void run()
{
....
waitCondition();
....
}
private void waitCondition()
{
synchronized(this)
{
while(!endWait)
{
try
{
// ждём пока другой поток не установит 'endWait' равным true
wait();
}
catch(InterruptedException e)
{
}
}
}
}
// это метод вызывается другим потоком чтобы разблокировать ждущий поток
public void wakeupThread()
{
synchronized(this)
{
// устанвливаем условие
endWait = true;
// разрешаем ждущему потоку выйти из wait()
notifyAll();
}
}
}
Если поле 'endWait' объявить без использования volatile, то компилятор может оптимизировать метод 'waitCondition' таким образом, что поток, исполняющий это метод, не будет реагировать на изменения этой переменной, сделанные другими потоками, то есть ждущий поток никогда не выйдет из метода 'endWait' (более подробное описание volatile и других особенностей модели памяти Java выходит за рамки этого поста).
Семен Бойков
опубликовал vmrobot ( авг 08 2006, 08:35:28 PM MSD ) Permalink Комментарии [8]

опубликовал Alexander Dolgin Август 09, 2006 at 11:18 AM MSD #
опубликовал Семён Бойков Август 09, 2006 at 06:01 PM MSD #
опубликовал Alexey Efimov Август 11, 2006 at 12:50 PM MSD #
опубликовал Alexandr Март 19, 2007 at 03:37 PM MSK #
опубликовал Vladimir Ivanov Март 20, 2007 at 01:57 AM MSK #
Ну нифига себе! Это что, половину программ чтоли надо переписывать? Мне кажется, такое поведение wait() ничем не обусловлено, поэтому подобная проверка должна делаться внутри wait(). Зачем программисту забивать говову проблемами, у него других хватает.
опубликовал null Август 09, 2007 at 12:29 PM MSD #
Моё исследование показало, что проблема заключается в следующем:
состояние любого потока в системе представленно определённым набором значений регистров, и тут резонно предположить, что для срабатывания операции приостанова потока в недрах функций блокировки (wait,sleep,join), кроме монитора java-объекта, у потока, эти самые регистры должны находиться в нужном для приостанова состоянии.
Так вот, при выходе из методов блокировки (wait,sleep,join) путём вызова у потока функции interrupt(), внутренние регистры потока переходят в такое состояние, которое в дальнейшем препятствует корректному приостанову потока функциями блокировки (wait,sleep,join).
Поэтому, решение проблемы заключается в том, чтобы вернуть регистры потока в нужное состояние.
Как показал опыт, вернуть их можно путём корректного завершения любого из методов блокировки (wait,sleep,join).
Из вешесказанного следует стабильное решение проблемы, без применения переменной endWait:
нужно изменить вызов метода sleep() следующим образом:
try {
sleep(Long.MAX_VALUE);
} catch(InterruptedException e1) {
while(true) {
try {
sleep(0, 1);
break;
} catch(InterruptedException e2) {}
}
}
P.S. Паузу в sleep(0, 1) делаем максимально малой, ибо не ради паузы мы его вызываем, а в качестве своеобразного восстановителя состояния регистров потока.
P.S. Опыт проводил на WindowsXPSP2 + jre1.6.0_07
--
С Уважением, Кондратенко Владимир
опубликовал Владимир Октябрь 23, 2008 at 03:15 PM MSD #
Уважаемый Владимир!
Покажите пожалуйста на примерах из вашего исследования, что это за "нужное состояние регистров" и как конкретно оно мешает остановке потока в wait/sleep/join. Достаточно примера для одной архитектуры/ОС.
опубликовал Кирилл Широков Октябрь 27, 2008 at 06:33 PM MSK #