Мне, похоже, хорошо известно среди разработчиков Swing моего знакомого, что invokeAndWait
проблематично, но, возможно, это не так хорошо известно, как я думал. Кажется, я вспомнил, что видел строгие предупреждения в документации о трудностях в правильном использовании invokeAndWait
, но мне трудно что-то найти. Я не могу найти ничего в текущей, официальной документации. Единственное, что мне удалось найти - это строка из старой версии Swing Tutorial от 2005 : (веб-архив)
Если вы используете invokeAndWait
, убедитесь, что поток, вызывающий invokeAndWait, не содержит блокировок, которые могут понадобиться другим потокам во время вызова.
К сожалению, эта строка, похоже, исчезла из текущего учебника по Swing. Даже это скорее преуменьшение; Я бы предпочел, чтобы в нем было что-то вроде: «Если вы используете invokeAndWait
, поток, вызывающий invokeAndWait
, не должен содержать какие-либо блокировки, которые могут понадобиться другим потокам во время вызова». В общем, трудно понять, какие блокировки могут понадобиться другим потокам в любой момент времени, поэтому наиболее безопасной политикой, вероятно, является обеспечение того, чтобы поток, вызывающий invokeAndWait
, вообще не удерживал никаких блокировок .
(Это довольно сложно сделать, и поэтому я сказал выше, что invokeAndWait
проблематично. Я также знаю, что разработчики JavaFX - по сути, замена Swing - определены в javafx.application. Платформа классифицирует метод с именем runLater
, который функционально эквивалентен invokeLater
. Но они сознательно пропустили эквивалентный метод invokeAndWait
, потому что его очень трудно правильно использовать.)
Причина довольно проста, чтобы быть выведенной из первых принципов. Рассмотрим систему, аналогичную описанной OP, с двумя потоками: MyThread и Thread Dispatch Thread (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, у нас снова тупик.
Это звучит довольно похоже на то, что происходит в приложении ОП. Согласно комментариям ОП на этот ответ ,
Да, renderOnEDT синхронизируется по какому-либо пути в стеке вызовов, метод com.acme.persistence.TransactionalSystemImpl.executeImpl, который синхронизируется. И renderOnEDT ожидает ввода того же метода. Итак, это источник тупика, на который это похоже. Теперь я должен выяснить, как это исправить.
У нас нет полной картины, но этого, вероятно, достаточно, чтобы продолжить. renderOnEDT
вызывается из MyThread, который удерживает блокировку чего-либо, пока он заблокирован в invokeAndWait
. Он ожидает обработки события EDT, но мы можем видеть, что EDT заблокирован чем-то, удерживаемым MyThread. Мы не можем точно сказать, какой именно это объект, но это не имеет значения - EDT явно заблокирован блокировкой, удерживаемой MyThread, и MyThread явно ожидает, пока EDT обработает событие: таким образом, тупик .
Обратите внимание, что мы можем быть уверены, что EDT в настоящее время не обрабатывает событие, отправленное invokeAndWait
(аналог E1 в моем сценарии выше).Если бы это было так, тупик возникал бы каждый раз.Кажется, что это происходит только иногда, и согласно комментарию от OP на этот ответ , когда пользователь быстро печатает.Так что я бы поспорил, что событие, обрабатываемое в настоящее время 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
из синхронизированного блока, так как это будеттребовать несинхронизированного доступа, получая state1 и state2.Вместо этого вы должны загрузить это состояние в локальные переменные, находясь в пределах блокировки, а затем выполнить вызов, используя эти локальные объекты после снятия блокировки.Что-то вроде:
int localState1;
String localState2;
synchronized (someObject) {
// code block 1
localState1 = state1;
localState2 = state2;
}
SwingUtilities.invokeAndWait(handler(localState1, localState2));
synchronized (someObject) {
// code block 2
}
Техника совершения звонков после снятия блокировок называется open call . См. Даг Ли, Параллельное программирование в Java (2-е издание), с. 2.4.1.3. Существует также хорошее обсуждение этой техники в работе Гетц и соавт. al., Java-параллелизм на практике , с. 10.1.4. Фактически весь раздел 10.1 довольно подробно описывает тупик; Я очень рекомендую это.
Таким образом, я считаю, что использование методов, которые я описал выше или в цитированных книгах, решит эту тупиковую проблему правильно и безопасно. Однако я уверен, что это потребует тщательного анализа и сложной реструктуризации. Я не вижу альтернативы.
(Наконец, я должен сказать, что, хотя я являюсь сотрудником Oracle, это никоим образом не является официальным заявлением Oracle.)
UPDATE
Я подумал о еще нескольких потенциальных рефакторингах, которые могли бы помочь решить проблему. Давайте пересмотрим исходную схему кода:
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
, является то, что он считывает некоторую информацию из GUI и затем использует ее для обновления общего состояния. Это должно быть в 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
}