Санкт-Петербургская группа тестирования JVM


« Новые оптимизации... | Main | Вышел Java Tutorial... »
20060808 вторник Август 08, 2006

"Случайный" выход из Object.wait()

Как хорошо всем изветно методы класса Object wait и notify служат для поддержки многопоточности языка Java и техника их использования также знакома всем, но что кажется несколько менее известным это случаи, при которых ждущий поток прерывает выполнение метода wait.

Вот вольный перевод части спецификации для метода wait(long timeout):

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

А вот что сказано ниже, двумя абзацами ниже:

«Также поток может быть разблокирован без уведомления, прерывания или истечении таймаута (так называемое случайное пробуждение, (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]

Trackback URL: http://blogs.sun.com/vmrobot/entry/%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D1%8B%D0%B9_%D0%B2%D1%8B%D1%85%D0%BE%D0%B4_%D0%B8%D0%B7_object_wait
Комментарии:

Если поле 'endWait' объявить без использования volatile... разве не гарантируется, что при входе в синхронизированный блок содержимое "памяти потока" будет синхронизировано с основной памятью?

опубликовал Alexander Dolgin Август 09, 2006 at 11:18 AM MSD #

Спасибо за комментарий, вы абсолютно правы - в этом примере использование volatile излишне: при вызове 'wait()' поток освобождает монитор, поток, который вызывает метод 'wakeupThread()', после вызова 'notifyAll()' выходит из синхронизированного блока, при этом проиходит синхронизация его памяти с основной. После того как был вызван 'notifyAll()', поток, выполняющий метод 'waitCondition()' при выходе из 'wait()' должен опять получить блокировку, что также должно привести к синхронизации кэша потока. Таким образом в этом примере поток, исполняющий метод waitCondition(), всегда видит изменения поля 'endWait'.

опубликовал Семён Бойков Август 09, 2006 at 06:01 PM MSD #

Вероятно это не совсем честный тест. После halt (System.exit) не что уже не гарантирует вам, что операционка корректно завершит процесс и все его треды. Вероятно по случайным выходом из wait подразумевается внешнее воздействие на спящий поток. Не из Java. Из отладчика или из операционки, где треды Java являются нативными и их можно "видеть". Поэтому, веротность этот спонтанного выхода нужно проверять именно внешним воздействием на треды. Например, из дебагера.

опубликовал Alexey Efimov Август 11, 2006 at 12:50 PM MSD #

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

опубликовал 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 #

Опубликовать комментарий:

Имя
E-Mail:
URL:

Ваш комментарий:

HTML Syntax: Отключен

Хиты страниц за сегодня: 10