Каждое приложение Java запускается с метода main(), который работает в главном потоке. В программе Swing главный поток живет кратковременно. Он планирует конструирование пользовательского интерфейса в потоке диспетчера событий и завершает свою работу.
После создания пользовательского интерфейса поток диспетчера событий обрабатывает уведомления о событиях наподобие вызовов actionPerformed() или paintComponent(). Другие потоки, такие как поток, отправляющий события в очередь, работают «за кулисами», но эти потоки невидимы прикладному программисту.
Раннее, в наших статьях, мы уже представили правило единственного потока: «не затрагивать компоненты Swing из любого потока, кроме потока диспетчера событий». Здесь мы глубже исследуем это правило.
Существует несколько исключений из этого правила:
- Вы можете совершенно безопасно добавлять и удалять слушатели событий в любом потоке. Конечно, методы слушателей будут вызваны из потока диспетчера событий.
- Небольшое количество методов Swing являются безопасными к потокам. Они специально помечены в документации по API предложением «This method is thread safe, although most Swing methods are not»(Этот метод безопасный к потокам, хотя большинство методов Swing таковыми не являются). Наиболее полезные из таких безопасных к потокам методов следующее:
- JTextComponent.setText()
- JTextArea.insert()
- JTextArea.append()
- JTextArea.replaceRange()
- JComponent.repaint()
- JComponent.revalidate()
В наших статьях мы многократно использовали метод repaint(), но метод revalidate() применяется реже. Его назначение — инициировать компоновку компонента после того, как изменилось содержимое. Традиционная библиотека AWT включает метод validate() для инициации перекомпоновки компонента. Для компонентов Swing вы должны вместо него вызывать revalidate().
Однако чтобы инициировать компоновку JFrame, вам придется вызвать validate(), потому что JFrame — это Component, а не JComponent.
Исторически правило единственного потока было более либеральным. Любой поток мог конструировать компоненты, устанавливать их свойство и добавлять в контейнеры — до тех пор, пока ни один из компонентов не был реализован. Компонент реализуется, когда начинает принимать события перерисовки и проверки достоверности. Это происходит, как только выполняется вызов setVisible(true) или pack() для этого компонента, либо когда компонент добавляется в уже реализованный контейнер.
Эта версия правила одного потока была удобной. Она позволяла создавать графический пользовательский интерфейс в методе main() и затем вызывать setVisible(true) на фрейме верхнего уровня приложения. Не было необходимости в утомительном планировании запуска через Runnable в потоке диспетчера событий.
К сожалению, некоторые реализаторы компонентов не уделили должного внимания тонкостям исходного правила одного потока. Они запускали действия в потоке диспетчера событий, не заботясь даже о проверке того, реализован ли компонент. Например, если вызывается setSelectionStart() или setSelectionEnd() на JTextComponent, то перемещения знака вставки планируются в потоке диспетчера событий, даже если компонент невидим.
Обнаружить и исправить эти проблемы совсем несложно, но проектировщики Swing пошли по пути наименьшего сопротивления. Они объявили, что обращаться к компонентам из любого потока, который не является потоком диспетчера событий, небезопасно. А поэтому вам нужно конструировать пользовательский интерфейс в потоке диспетчера событий, используя вызов EventQueue.invokeLater(), который вы уже не раз видели в наших примерах программ.
Конечно, существует масса программ, которые не соблюдают осторожности и руководствуются старыми представлениями о правиле одного потока, инициализируя пользовательский интерфейс в главном потоке.
Эти программы подвержены некоторому риску, связанному с тем, что определение действия по инициализации пользовательского интерфейса вызовут конфликт с действиями главного потока. Вы вряд-ли захотите оказаться одним из немногих «счастливчиков», которые столкнутся с неприятностями и будут вынуждены тратить время на отладку нерегулярно воспроизводящих ошибок, связанных с потоками. Поэтому лучше просто следовать правилу единственного потока.