Java: библиотеки Swing и безопасность потоков

Я часто слышал критику по поводу отсутствия безопасности потоков в библиотеках Swing. Тем не менее, я не уверен, что то, что я буду делать в своем собственном коде, может вызвать проблемы:

В каких ситуациях в игру вступает тот факт, что Swing не безопасен для потоков?

Что я должен активно избегать?

11 ответов

Решение
  1. Никогда не выполняйте долго выполняющиеся задачи в ответ на кнопку, событие и т. Д., Так как они находятся в потоке событий. Если вы заблокируете поток событий, ВЕСЬ графический пользовательский интерфейс будет полностью не отвечать, в результате чего ДЕЙСТВИТЕЛЬНО разозлится пользователь. Вот почему Свинг кажется медленным и резким.

  2. Используйте Threads, Executors и SwingWorker для запуска задач НЕ НА EDT (поток рассылки событий).

  3. Не обновляйте и не создавайте виджеты за пределами EDT. Единственный вызов, который вы можете сделать вне EDT - это Component.repaint(). Используйте SwingUtilitis.invokeLater, чтобы гарантировать выполнение определенного кода в EDT.

  4. Используйте методы отладки EDT и продуманный внешний вид (например, Substance, которая проверяет нарушение EDT)

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

Пример действительно ДЕЙСТВИТЕЛЬНОЙ работы Swing UI: Palantir Technologies. Примечание: я НЕ работаю на них, просто пример потрясающего свинга. Не позорьте публичную демонстрацию... Их блог тоже хорош, редок, но хорош

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

Все, что получает доступ к состоянию java.awt.Component должен запускаться внутри EDT, с тремя исключениями: все, что конкретно задокументировано как поточно-ориентированное, например repaint(), revalidate(), а также invalidate(); любой компонент в пользовательском интерфейсе, который еще не реализован; и любой компонент в апплете до этого апплета start() был вызван.

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

Реализованный означает, что Компонент является либо контейнером верхнего уровня (например, JFrame), на котором любой из setVisible(true), show(), или же pack() был вызван или был добавлен к реализованному Компоненту. Это означает, что совершенно нормально построить свой интерфейс в методе main(), как это делают многие учебные примеры, поскольку они не вызывают setVisible(true) в контейнере верхнего уровня, пока каждый компонент не будет добавлен в него, не настроены шрифты и границы и т. д.

По тем же причинам совершенно безопасно создать свой интерфейс апплета в своем init() метод, а затем вызвать start() после того, как все это построено.

Оборачивание последующих изменений компонентов в Runnables для отправки invokeLater() становится легко получить права после всего лишь несколько раз. Единственное, что меня раздражает, так это чтение состояния компонента (скажем, someTextField.getText()) из другой ветки. Технически это должно быть завернуто в invokeLater(), тоже; на практике это может сделать уродливый код быстрым, и я часто не беспокоюсь об этом, или я осторожен, чтобы получить эту информацию во время первоначальной обработки события (как правило, в большинстве случаев самое подходящее время для этого).

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

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

class MyApp {
    public static void main(String[] args) {
        java.awt.EventQueue.invokeLater(new Runnable() { public void run() {
            runEDT();
        }});
    }
    private static void runEDT() {
        assert java.awt.EventQueue.isDispatchThread();
        ...

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

public final static void checkOnEventDispatchThread() {
    if (!SwingUtilities.isEventDispatchThread()) {
        throw new RuntimeException("This method can only be run on the EDT");
    }
}

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

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

Активно избегайте любой работы Swing, кроме как в потоке диспетчеризации событий. Swing был написан так, чтобы его было легко расширять, и Sun решила, что однопоточная модель лучше для этого.

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

Фраза "нить-небезопасный" звучит так, как будто есть что-то плохое по сути (вы знаете... "безопасно" - хорошо; "небезопасно" - плохо). Реальность такова, что безопасность потоков стоит дорого - безопасные для потоков объекты зачастую гораздо сложнее реализовать (а Swing достаточно сложен, даже если он есть).

Кроме того, потокобезопасность достигается с помощью стратегий блокировки (медленная) или сравнения и замены (сложная). Учитывая, что GUI взаимодействует с людьми, которые, как правило, непредсказуемы и их трудно синхронизировать, многие инструментарии решили направить все события через один насос событий. Это верно для Windows, Swing, SWT, GTK и, возможно, других. На самом деле я не знаю ни одного инструментария с графическим интерфейсом, который действительно поточно-ориентирован (то есть вы можете манипулировать внутренним состоянием его объектов из любого потока).

Вместо этого обычно делается то, что GUI предоставляют способ справиться с поточной небезопасностью. Как уже отмечалось, Swing всегда предоставлял несколько упрощенный SwingUtilities.invokeLater(). Java 6 включает превосходный SwingWorker (доступный для предыдущих версий на Swinglabs.org). Существуют также сторонние библиотеки, такие как Foxtrot, для управления потоками в контексте Swing.

Известность Swing заключается в том, что дизайнеры приняли легкомысленный подход, предполагая, что разработчик будет делать правильные вещи и не останавливать EDT или модифицировать компоненты извне EDT. Они заявили о своей политике многоплановости громко и четко, и разработчики должны следовать ей.

Тривиально заставить каждый API свинга отправлять работу в EDT для каждого набора свойств, аннулировать и т. Д., Что сделает его поточно-ориентированным, но за счет значительных замедлений. Вы даже можете сделать это самостоятельно, используя АОП. Для сравнения, SWT генерирует исключения, когда к компоненту обращаются из неправильного потока.

Если вы используете Java 6, то SwingWorker, безусловно, самый простой способ справиться с этим.

По сути, вы хотите убедиться, что все, что изменяет пользовательский интерфейс, выполняется в EventDispatchThread.

Это можно найти с помощью метода SwingUtilities.isEventDispatchThread(), чтобы сообщить вам, если вы в нем (как правило, не очень хорошая идея - вы должны знать, какой поток активен).

Если вы не участвуете в EDT, вы используете SwingUtilities.invokeLater() и SwingUtilities.invokeAndWait() для вызова Runnable в EDT.

Если вы обновляете пользовательский интерфейс не на EDT, вы получаете невероятно странное поведение. Лично я не считаю это недостатком Swing, вы получаете хорошую эффективность, не синхронизируя все потоки, чтобы обеспечить обновление пользовательского интерфейса - вам просто нужно запомнить это предупреждение.

Для более подробной информации о потоках, "Укрощение потоков Java" Аллена Холуба - более старая книга, но она отлично читается.

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

http://www.amazon.com/Taming-Java-Threads-Allen-Holub/dp/1893115100 http://www.holub.com/software/taming.java.threads.html

Люблю раздел "Если бы я был королем" в конце там.

Вот образец для того, чтобы сделать качание нить-дружески.

Подкласс Action (MyAction) и сделайте его doAction многопоточным. Заставьте конструктора взять имя строки.

Дайте ему абстрактный метод actionImpl().

Пусть это будет похоже на... (предупреждение псевдокода!)

doAction(){
new Thread(){
   public void run(){
    //kick off thread to do actionImpl().
       actionImpl();
       MyAction.this.interrupt();
   }.start();  // use a worker pool if you care about garbage.
try {
sleep(300);
Go to a busy cursor
sleep(600);
Show a busy dialog(Name) // name comes in handy here
} catch( interrupted exception){
  show normal cursor
}

Вы можете записать время, затраченное на выполнение задачи, и в следующий раз ваш диалог может показать приличную оценку.

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

invokeLater() и invokeAndWait() действительно ДОЛЖНЫ использоваться, когда вы выполняете какое-либо взаимодействие с компонентами GUI из любого потока, который НЕ является EDT.

Это может работать во время разработки, но, как и большинство одновременных ошибок, вы начнете видеть странные исключения, которые кажутся совершенно не связанными и происходят недетерминированно - обычно обнаруживаются ПОСЛЕ того, как вас отправили реальные пользователи. Нехорошо.

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

Да, это уродливо оборачивает каждый вызов метода обратно в EDT в экземпляре Runnable, но это Java для вас. Пока мы не получим закрытия, вам просто придется с этим жить.

Обратите внимание, что даже интерфейсы модели не являются поточно-ориентированными. Размер и содержимое запрашиваются с помощью отдельных методов get, поэтому их невозможно синхронизировать.

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

Обновление состояния модели всегда в EDT позволяет избежать этого.

Другие вопросы по тегам