Похоже, среди моих знакомых разработчиков Swing хорошо известно, что invokeAndWait
проблематичен, но, возможно, это не так хорошо известно, как я думал. Кажется, я припоминаю, что видел в документации строгие предупреждения о трудностях правильного использования invokeAndWait
, но мне трудно что-либо найти. Я не могу найти ничего в текущей официальной документации. Единственное, что мне удалось найти, это эту строку из старой версии Учебное пособие по Swing 2005 года: (веб-архив)
Если вы используете invokeAndWait
, убедитесь, что поток, вызывающий invokeAndWait, не содержит блокировок, которые могут понадобиться другим потокам во время выполнения вызова.
К сожалению, эта строка, похоже, исчезла из текущего руководства по Swing. Даже это, скорее, преуменьшение; Я бы предпочел, чтобы он говорил что-то вроде: «Если вы используете invokeAndWait
, поток, который вызывает invokeAndWait
, не должен удерживать какие-либо блокировки, которые могут понадобиться другим потокам во время выполнения вызова». В общем, трудно узнать, какие блокировки могут понадобиться другим потокам в любой момент времени, самая безопасная политика, вероятно, состоит в том, чтобы убедиться, что поток, вызывающий invokeAndWait
, вообще не удерживает никаких блокировок.
(Это довольно сложно сделать, и именно поэтому я сказал выше, что invokeAndWait
проблематичен. Я также знаю, что разработчики JavaFX — по сути замены Swing — определили в javafx.application.Platform класс метод с именем runLater
, который функционально эквивалентен invokeLater
. Но они намеренно опустил метод, эквивалентный invokeAndWait
, потому что его очень сложно правильно использовать.)
Причина довольно проста, чтобы вывести из первых принципов. Рассмотрим систему, аналогичную той, что описана в OP, с двумя потоками: MyThread и потоком отправки событий (EDT). MyThread блокирует объект L и затем вызывает invokeAndWait
. Это отправляет событие E1 и ожидает его обработки EDT. Предположим, что обработчику E1 необходимо заблокировать L. Когда EDT обрабатывает событие E1, он пытается заблокировать L. Эта блокировка уже удерживается MyThread, который не освободит ее, пока EDT не обработает E1, но эта обработка заблокирована. от MyThread. Таким образом, мы имеем тупик.
Вот вариант этого сценария. Предположим, мы гарантируем, что обработка E1 не требует блокировки L. Будет ли это безопасно? Нет. Проблема все еще может возникнуть, если непосредственно перед вызовом MyThread invokeAndWait
событие E0 отправляется в очередь событий, а обработчик E0 требует блокировки L. Как и прежде, MyThread удерживает блокировку L, поэтому обработка E0 блокируется. E1 находится позади E0 в очереди событий, поэтому обработка E1 также заблокирована. Поскольку MyThread ожидает обработки E1, а он заблокирован E0, который, в свою очередь, заблокирован, ожидая, пока MyThread освободит блокировку L, у нас снова взаимоблокировка.
Это звучит довольно похоже на то, что происходит в приложении OP. Согласно комментариям ОП к этому ответу,
Да, renderOnEDT синхронизируется с чем-то выше в стеке вызовов, с синхронизированным методом com.acme.persistence.TransactionalSystemImpl.executeImpl. И renderOnEDT ждет входа в тот же метод. Итак, это источник тупика, на который похоже. Теперь мне нужно понять, как это исправить.
У нас нет полной картины, но этого, вероятно, достаточно, чтобы продолжить. renderOnEDT
вызывается из MyThread, который удерживает блокировку чего-то, пока оно заблокировано в invokeAndWait
. Он ожидает обработки события EDT, но мы видим, что EDT заблокирован на чем-то, что хранится в MyThread. Мы не можем точно сказать, что это за объект, но это не имеет значения — EDT явно заблокирован на блокировке, удерживаемой MyThread, и MyThread явно ожидает, пока EDT обработает событие: таким образом, взаимоблокировка .
Также обратите внимание, что мы можем быть уверены, что EDT в настоящее время не обрабатывает событие, отправленное invokeAndWait
(аналогично E1 в моем сценарии выше). Если бы это было так, взаимоблокировка происходила бы каждый раз. Кажется, это происходит только иногда и, согласно комментарию ОП к этому ответу, когда пользователь быстро печатает. Поэтому я готов поспорить, что событие, которое в настоящее время обрабатывается EDT, представляет собой нажатие клавиши, которое было отправлено в очередь событий после того, как MyThread взял свою блокировку, но до того, как MyThread вызвал invokeAndWait
для отправки E1 в очередь событий, таким образом, это аналогично E0 в моем сценарии выше.
Пока что это, вероятно, в основном краткое изложение проблемы, собранное из других ответов и из комментариев ОП к этим ответам. Прежде чем мы перейдем к обсуждению решения, вот несколько предположений, которые я делаю о приложении OP:
Он многопоточный, поэтому для правильной работы различные объекты должны быть синхронизированы. Сюда входят вызовы обработчиков событий Swing, которые предположительно обновляют некоторую модель на основе взаимодействия с пользователем, и эта модель также обрабатывается рабочими потоками, такими как MyThread. Поэтому они должны правильно блокировать такие объекты. Удаление синхронизации, безусловно, позволит избежать взаимоблокировок, но другие ошибки будут появляться, поскольку структуры данных будут повреждены несинхронизированным одновременным доступом.
Приложение не обязательно выполняет длительные операции в EDT. Это типичная проблема с приложениями с графическим интерфейсом, но здесь, похоже, этого не происходит. Я предполагаю, что приложение работает нормально в большинстве случаев, когда событие, обрабатываемое в EDT, захватывает блокировку, что-то обновляет, а затем освобождает блокировку. Проблема возникает, когда он не может получить замок, потому что держатель замка заблокирован на EDT.
Изменение invokeAndWait
на invokeLater
не вариант. ОП сказал, что это вызывает другие проблемы. Это неудивительно, так как это изменение приводит к тому, что выполнение происходит в другом порядке, поэтому оно дает другие результаты. Я предполагаю, что они были бы неприемлемы.
Если мы не можем удалить блокировки и не можем измениться на invokeLater
, нам остается безопасно вызывать invokeAndWait
. И «безопасно» означает отказ от блокировок перед вызовом. Это может быть сколь угодно сложно сделать, учитывая организацию приложения OP, но я думаю, что это единственный способ продолжить.
Давайте посмотрим, что делает MyThread. Это очень упрощено, так как в стеке, вероятно, есть куча промежуточных вызовов методов, но в основном это примерно так:
synchronized (someObject) {
// code block 1
SwingUtilities.invokeAndWait(handler);
// code block 2
}
Проблема возникает, когда какое-то событие пробирается в очередь перед обработчиком, и для обработки этого события требуется блокировка someObject
. Как мы можем избежать этой проблемы? Вы не можете отказаться от одной из встроенных в Java блокировок монитора в блоке synchronized
, поэтому вам нужно закрыть блок, сделать вызов и открыть его снова:
synchronized (someObject) {
// code block 1
}
SwingUtilities.invokeAndWait(handler);
synchronized (someObject) {
// code block 2
}
Это может быть сколь угодно сложно, если блокировка someObject
берется довольно далеко вверх по стеку вызовов от вызова к invokeAndWait
, но я думаю, что такой рефакторинг неизбежен.
Есть и другие подводные камни. Если кодовый блок 2 зависит от некоторого состояния, загруженного кодовым блоком 1, это состояние может устареть к тому времени, когда кодовый блок 2 снова возьмет блокировку. Это означает, что блок кода 2 должен перезагрузить любое состояние из синхронизированного объекта. Он не должен делать никаких предположений на основе результатов блока кода 1, поскольку эти результаты могут быть устаревшими.
Вот еще одна проблема. Предположим, что обработчику, запускаемому invokeAndWait
, требуется некоторое состояние, загруженное из общего объекта, например,
synchronized (someObject) {
// code block 1
SwingUtilities.invokeAndWait(handler(state1, state2));
// code block 2
}
Вы не могли просто перенести вызов invokeAndWait
из синхронизированного блока, так как это потребовало бы несинхронизированного доступа для получения состояния1 и состояния2. Вместо этого вам нужно загрузить это состояние в локальные переменные, находясь внутри блокировки, а затем выполнить вызов, используя эти локальные переменные после снятия блокировки. Что-то типа:
int localState1;
String localState2;
synchronized (someObject) {
// code block 1
localState1 = state1;
localState2 = state2;
}
SwingUtilities.invokeAndWait(handler(localState1, localState2));
synchronized (someObject) {
// code block 2
}
Техника осуществления вызовов после снятия блокировок называется методом открытого вызова. См. Дуг Ли, Параллельное программирование на Java (2-е издание), раздел 2.4.1.3. Также есть хорошее обсуждение этой техники в Goetz et. и др., Параллелизм в Java на практике, раздел 10.1.4. На самом деле весь раздел 10.1 довольно подробно описывает взаимоблокировку; Я очень рекомендую это.
Подводя итог, я считаю, что использование методов, которые я описал выше или в цитируемых книгах, решит эту проблему взаимоблокировки правильно и безопасно. Однако я уверен, что это потребует тщательного анализа и сложной реструктуризации. Хотя я не вижу альтернативы.
(Наконец, я должен сказать, что, хотя я являюсь сотрудником Oracle, это никоим образом не является официальным заявлением Oracle.)
ОБНОВЛЕНИЕ
Я подумал о еще паре потенциальных рефакторингов, которые могли бы помочь решить проблему. Давайте пересмотрим исходную схему кода:
synchronized (someObject) {
// code block 1
SwingUtilities.invokeAndWait(handler);
// code block 2
}
Это выполняет блок кода 1, обработчик и блок кода 2 по порядку. Если бы мы изменили вызов invokeAndWait
на invokeLater
, обработчик был бы выполнен после блока кода 2. Легко видеть, что это было бы проблемой для приложения. Вместо этого, как насчет того, чтобы переместить блок кода 2 в invokeAndWait
, чтобы он выполнялся в правильном порядке, но все еще в потоке событий?
synchronized (someObject) {
// code block 1
}
SwingUtilities.invokeAndWait(Runnable {
synchronized (someObject) {
handler();
// code block 2
}
});
Вот еще один подход. Я не знаю точно, для чего предназначен обработчик, переданный invokeAndWait
. Но одна из причин, по которой может потребоваться invokeAndWait
, заключается в том, что он считывает некоторую информацию из графического интерфейса, а затем использует ее для обновления общего состояния. Это должно быть в EDT, так как оно взаимодействует с объектами GUI, и invokeLater
нельзя использовать, так как это произойдет в неправильном порядке. Это предполагает вызов invokeAndWait
перед выполнением другой обработки, чтобы считать информацию из GUI во временную область, а затем использовать эту временную область для продолжения обработки:
TempState tempState;
SwingUtilities.invokeAndWait(Runnable() {
synchronized (someObject) {
handler();
tempState.update();
}
);
synchronized (someObject) {
// code block 1
// instead of invokeAndWait, use tempState from above
// code block 2
}
person
Stuart Marks
schedule
06.12.2013