70 |
ГЛАВА 3 |
Таким образом, вы (в отдельном потоке выполнения) можете провести какие-либо сложные вычисления, изменить некоторые данные, а затем попросить компонент обновить себя, так чтобы он смог вывести на экран новую информацию. И это не приведет к конфликтам.
Ну и, наконец, есть самый гибкий способ изменить компонент из другого потока. Если ваша задача, выполняющаяся в отдельном потоке, так или иначе приводит к необходимости изменить что-либо в компоненте, и в этом вам не могут помочь безопасные вызовы, остается только одно. Во избежание неприятностей с потоком рассылки событий и неожиданного тупика все действия с компонентами все равно нужно выполнять из потока рассылки событий. И у вас есть возможность выполнить некоторое действие в потоке рассылки событий. Для этого предназначены методы invokeLater() и invokeAndWait() класса EventQueue. Данным методам нужно передать ссылку на интерфейс Runnable, метод run() этого интерфейса будет выполнен потоком рассылки событий, как только он доберется до него. (Переданная в метод invokeLater() или invokeAndWait() ссылка на интерфейс Runnable «оборачивается» в событие специального типа InvocationEvent, обработка которого сводится к вызову метода run().) В итоге вы действуете следующим образом: выполняете в отдельном потоке сложные долгие вычисления, получаете некоторые результаты, а действия, которые необходимо провести после этого с графическими компонентами, выполняете в потоке рассылки событий с помощью метода invokeLater() или invokeAndWait(). Рассмотрим небольшой пример:
//InvokeLater.java
//Метод invokeLater() и работа с потоком рассылки событий import java.awt.*;
import java.awt.event.*; import javax.swing.*;
public class InvokeLater extends JFrame { public InvokeLater() {
super("InvokeLater");
//при закрытии окна - выход setDefaultCloseOperation(EXIT_ON_CLOSE);
//добавим кнопку со слушателем
button = new JButton("Выполнить сложную работу"); button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) { // запустим отдельный поток
new ComplexJobThread().start(); button.setText("Подождите...");
}
}); // настроим панель содержимого и выведем окно на экран
setLayout(new FlowLayout()); add(new JTextField(20)); add(button);
setSize(300, 200);
За кулисами системы обработки событий |
71 |
setVisible(true);
}
private JButton button;
// поток, выполняющий "сложную работу" class ComplexJobThread extends Thread {
public void run() { try {
//изобразим задержку sleep(3000);
//работа закончена, нужно изменить интерфейс
EventQueue.invokeLater(new Runnable() { public void run() {
button.setText("Работа завершена");
}
});
}catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new InvokeLater(); } });
}
}
Мы создаем небольшое окно, в панели содержимого которого размещается кнопка JButton и вспомогательное текстовое поле. При нажатии кнопки будет вызван слушатель ActionListener, и мы предполагаем, что работа, которая предстоит слушателю, довольно сложна и займет приличное время, поэтому выполнять ее нужно в отдельном потоке (на самом деле, выполнение долгих вычислений в слушателе, который был вызван потоком рассылки событий, приведет к блокированию остальных событий, ждущих в очереди рассылки тем же потоком, и в итоге интерфейс программы станет неотзывчивым). Отдельный поток, выполняющий долгие сложные вычисления, реализован во внутреннем классе ComplexJobThread, унаследованном от базового класса всех потоков Thread. Проблема состоит в том, что по окончании вычислений нам нужно сменить надпись на кнопке, а мы к этому моменту будем находиться в отдельном потоке. Менять надпись из потока, отличного от потока рассылки событий, не стоит: это может привести к неопределенности данных в компонентах и ошибкам, и даже к тупикам
ивзаимным блокировкам потоков (мы уже знаем, что компоненты Swing не обладают встроенной синхронизацией). В момент обращения другого потока к компоненту последний вполне может получить событие о щелчке мыши от потока рассылки событий
иприйти в нестабильное состояние.
Здесь нам и пригодится статический метод invokeLater() класса EventQueue. Он позволяет выполнить некоторый фрагмент кода из потока рассылки событий. В качестве
72 ГЛАВА 3
параметра данному методу нужно передать ссылку на объект, который реализует интерфейс Runnable: метод run(), определенный в этом интерфейсе, и будет выполнен потоком рассылки событий. Так мы и поступаем в примере: код, меняющий надпись на кнопке, будет выполнен из потока рассылки событий, так что можно не опасаться возникновения конфликтов. Запустив программу с примером и нажав кнопку, вы увидите, что во время вычислений пользовательский интерфейс доступен (вы сможете набрать что-ли- бо в текстовом поле или нажать кнопку еще раз). Сразу по завершении вычислений вы узнаете об этом по изменению надписи не кнопке, и никаких конфликтов при этом не возникнет.
Помимо метода invokeLater() в вашем распоряжении также имеется метод invokeAndWait(). Он аналогичным образом позволяет выполнить фрагмент кода из потока рассылки событий, но в отличие от invokeLater(), делает это синхронно: приостанавливая работу потока, из которого вы его вызвали, до тех пор, пока поток рассылки событий не выполнит ваш код. С другой стороны, invokeLater() работает асинхронно: он немедленно возвращает вам управление, так что вы можете продолжать работу, зная, что рано или поздно ваш фрагмент кода будет выполнен потоком рассылки событий. В большинстве ситуаций предпочтительнее метод invokeLater(), а метод invokeAndWait() стоит использовать только там, где немедленное выполнение потоком рассылки фрагмента вашего кода обязательно для дальнейших действий. Работая с методом invokeAndWait(), следует быть внимательнее: поток, из которого вы его вызвали, будет ожидать, когда поток рассылки событий выполнит переданные ему фрагмент кода, а в этом фрагменте кода могут быть обращения к ресурсам, принадлежащим первому потоку, тому самому, что находится в ожидании. В итоге возникнет взаимная блокировка, и программа окажется в тупике. Метод invokeLater() позволяет всего этого избежать.
Практически все компоненты Swing не обладают встроенной синхронизацией, но благодаря описанным двум методам класса EventQueue вы всегда сможете выполнить некоторые действия с компонентами из потока рассылки событий. Правда, есть несколько исключений, к примеру, текстовые компоненты Swing, такие как многострочные поля JTextArea или редакторы JEditorPane, позволяют изменять свой текст из другого потока (и это очень удобно, особенно при загрузке больших текстов). Таких исключений немного, и если компонент может работать в многозадачном окружении, вы увидите упоминание об этом в его интерактивной документации и в этой книге.
Принципы работы потока рассылки событий и рассмотренный только что пример плавно подводят нас к «золотому правилу» Swing: пишите слушатели короткими и быстрыми. Здесь все просто: рассылка событий и вызов соответствующих слушателей, в которых они обрабатываются, происходят из одного потока выполнения, потока рассылки событий EventDispatchThread. Как только какой-либо слушатель начинает производить долгие вычисления, все остальные события «застревают» в очереди событий, и интерфейс программы перестает отзываться на любые действия пользователя, а работать с такой неповоротливой программой — удовольствие ниже среднего.
СОВЕТ
Если в слушателе или методе обработки событий выполняются достаточно длинные вычисления, выносите их в отдельный поток. Манипулировать графическими компонентами из другого потока выполнения вы всегда сможете с помощью методов класса EventQueue или класса SwingUtilities из пакета javax.swing. Последний позволяет избежать импорта ненужных пакетов AWT и именно его рекомендуется применять в Swing .
За кулисами системы обработки событий |
73 |
Следуя этому нехитрому совету, с помощью Swing вы всегда будете писать эффектные и скоростные пользовательские интерфейсы. Зачастую незнание этого правила приводит к написанию чудовищно неповоротливых программ, а вину за это их создатели сваливают на Swing и Java, которые, тем не менее, позволяют разрабатывать интерфейсы, работающие не менее быстро, чем интерфейсы приложений «родной» операционной системы, что неоднократно доказано на практике даже очень большими приложениями.
Отзывчивость программы и SwingWorker
Мы уже поняли, что выполнение долгих вычислений и логики в отдельном потоке позволяет обеспечить быстрый отклик компонентов Swing и всего интерфейса к большому удовольствию его пользователей, а проблему конфликтов позволяет решить исполнение задач в потоке рассылки событий с помощью класса EventQueue или SwingUtilites. Однако это все потребовало от нас изучить изнанку системы событий Swing и напрямую манипулировать потоками.
В библиотеке Swing имеется класс, способный облегчить нашу участь, и скрыть некоторые детали процесса за своими понятными методами. Он называется SwingWorker, и задача его состоит в том, что элегантно разделить исполнение основной задачи, обновление промежуточных результатов в пользовательском интерфейсе (конечно же, из потока рассылки событий) и позволить выполнить какие-либо действия с интерфейсом после окончания вычислений.
Давайте попробуем переделать предыдущий пример с «долгими» вычислениями
спомощью SwingWorker. Вот что у нас получится:
//UsingSwingWorker.java
//Класс SwingWorker для отзывчивости интерфейса import javax.swing.*;
import java.awt.*; import java.awt.event.*; import java.util.List;
public class UsingSwingWorker extends JFrame { private JButton button;
public UsingSwingWorker() { super("UsingSwingWorker");
//при закрытии окна - выход setDefaultCloseOperation(EXIT_ON_CLOSE);
//добавим кнопку со слушателем
button = new JButton("Выполнить сложную работу"); button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) { // запустим отдельную долгую работу
new ComplexJob().execute(); button.setText("Подождите...");
}
});
74 |
ГЛАВА 3 |
// настроим панель содержимого и выведем окно на экран setLayout(new FlowLayout());
add(new JTextField(20)); add(button); setSize(300, 200); setVisible(true);
}
// класс, выполняющий "сложную работу"
class ComplexJob extends SwingWorker<String,String> { // здесь выполняется работа, это отдельный поток! public String doInBackground() throws Exception {
Thread.sleep(2000);
// публикуем промежуточные результаты publish("Половина работы закончена..."); Thread.sleep(2000);
return "";
}
//обработка промежуточных результатов
//это поток рассылки событий!
protected void process(List<String> chunks) { button.setText(chunks.get(0));
}
// окончание работы - и вновь это поток рассылки public void done() {
button.setText("Работа завершена");
}
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new UsingSwingWorker(); } });
}
}
Мы вновь создаем небольшое окно, в панели содержимого которого размещается кнопка JButton и вспомогательное текстовое поле. При нажатии кнопки нас ожидает тяжелая, долгая, неповоротливая задача, и во имя наших пользователей мы обязаны выполнить ее вне потока рассылки событий. Теперь нам помогает класс SwingWorker, специально предназначенный для этих задач.
Прежде всего, стоит отметить, что класс этот абстрактный и требует чтобы от него всегда наследовали, а также он позволяет настроить типы данных, с которыми он будет работать. В нашем примере мы выбираем для данных просто строки. Первый тип данных — это то, что будет возвращать в качестве результата наша задача. Так как у нас пример умозрительный, мы ничего не возвращаем и используем пустую строку. Второй