При участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
Скачать 4.97 Mb.
|
Глава 9 Приложения GUI Если вы когда-либо пытались написать даже простое приложение GUI с помощью Swing, вы знаете, что GUI-приложения имеют собственные своеобразные проблемы с потоками. Для обеспечения безопасности, определённые задачи должны выполняться в потоке событий Swing. Но вы не можете выполнять длительные задачи в потоке событий из-за боязни того, что пользовательский интерфейс перестанет отвечать на запросы. И структуры данных Swing не являются потокобезопасными, поэтому необходимо быть осторожными, ограничивая их рамками потока событий. Почти все наборы инструментов GUI, включая Swing и SWT, реализованы в виде однопоточных подсистем, в которых все действия GUI ограничены одним потоком. Если вы не планируете писать полностью однопоточную программу, в ней будут активности, которые будут частично выполняться в потоке приложения, а частично - в потоке событий. Подобно многим другим ошибкам многопоточности, неправильное разделение между этими частями не обязательно приведёт к немедленному возникновению аварии в вашей программе; вместо этого, программа может вести себя странно при сложно уловимых условиях. Несмотря на то, что фреймворки GUI сами по себе являются однопоточными подсистемами, ваше приложение может не являться таковым, и вам все равно придётся тщательно рассматривать проблемы с потоками при написании кода GUI. 9.1 Почему фреймворки GUI однопоточны? В прошлом, приложения GUI были однопоточными, и события GUI обрабатывались из "основного цикла обработки событий". Современные фреймворки GUI используют немного отличающуюся модель: они создают выделенный поток диспетчеризации событий (EDT, event dispatch thread) для обработки событий GUI. Однопоточные фреймворки GUI не являются уникальным явлением, присущим только Java; Qt, NextStep, MacOS Cocoa, X Windows, и многие другие также однопоточны. Сложившаяся ситуация является таковой не из-за отсутствия попыток; было предпринято множество попыток написать многопоточные фреймворки GUI, но из-за постоянных проблем с условиями гонок и взаимоблокировками, все они, в конечном итоге, пришли к однопоточной модели с очередью событий, в которой выделенный поток извлекает события из очереди и отправляет их обработчикам событий, определённым приложением. (Фреймворк AWT изначально пытался в большей степени поддерживать многопоточный доступ, и решение сделать Swing однопоточным, было, в основном, основано на опыте, полученном при работе с AWT.) Многопоточные фреймворки GUI, как правило, особенно чувствительны к взаимоблокировкам, частично из-за неудачного взаимодействия между механизмом обработки входящих событий и объектно-ориентированной моделью компонентов GUI. Действия, инициируемые пользователем, как правило “всплывают” от ОС к приложению — щелчок мыши обнаруживается ОС, превращается c помощью инструментария в событие “щелчок мыши” и, в конечном итоге, доставляется слушателю приложения как событие более высокого уровня, такое как ”нажатие кнопки". С другой стороны, действия инициируемые приложением “проваливаются” от приложения в ОС - изменение цвета фона компонента происходит в приложении и отправляется определенному классу компонента, и, в конечном итоге, в ОС для визуализации. Сочетание тенденции получения активностями доступа к одним и тем же объектам GUI в противоположном порядке, с требованием сделать каждый объект потокобезопасным, порождает рецепт возникновения несогласованного порядка блокировок, что приводит к возникновению взаимоблокировок (см. главу 10 ). И это именно тот опыт, который, всякий раз в процессе разработки, переоткрывают для себя почти все инструментарии GUI. Другим фактором, приводящим к возникновению взаимоблокировок в многопоточных фреймворках GUI, является преобладание шаблона модель- представление-контроллер (model-view-controler, MVC). Факторинг взаимодействий пользователя в кооперацию объектов, представляющих модель, представление и контроллер, значительно упрощает реализацию приложений GUI, но вновь повышает риск несогласованного упорядочивания блокировок. Контроллер отправляет вызов в модель, которая уведомляет представление о том, что что-то изменилось. Но контроллер также может отправить вызов в представление, которое, в свою очередь, может отправить вызов в модель для запроса состояния модели. В результате будет получен несогласованный порядок блокировки, с сопутствующим риском возникновения взаимоблокировки. В своём блоге 103 , Sun VP Грэм Гамильтон отлично подводит итоги по вышеприведённым проблемам, описывая, почему многопоточный инструментарий GUI является повторяющейся "провальной мечтой" компьютерной науки. Я считаю, что вы можете успешно программировать с использованием многопоточного инструментария GUI, если инструментарий спроектирован очень тщательно; если инструментарий во всех деталях раскрывает используемую методологию блокировки; если вы очень умны, очень осторожны, и у вас есть глобальное понимание всей структуры инструментария. Если вы хотя-бы немного ошибётесь в одной из этих вещей, все будет в основном работать, но вы получите случайные зависания (из-за взаимоблокировок) или глюки (из-за условий гонок). Такой многопоточный подход лучше всего подходит для людей, которые принимали непосредственное участие в разработке инструментария. К сожалению, я не думаю, что перечисленный набор характеристик подходит для широкого коммерческого использования. Вы создаёте приложение, которое работает не совсем надежно по причинам, которые вовсе не очевидны, силами нормальных умных программистов. Таким образом, авторы приложения очень недовольны и разочарованы и употребляют плохие слова по адресу бедного невинного инструментария. Однопоточные фреймворки GUI обеспечивают потокобезопасность посредством ограничения потока; все объекты GUI, включая визуальные компоненты и модели данных, доступны исключительно из потока событий. Конечно, это просто переносит часть нагрузки по обеспечению потокобезопасности обратно на плечи разработчика приложения, который должен убедиться, что объекты ограничены правильным образом. 103 http://weblogs.java.net/blog/kgh/archive/2004/10 9.1.1 Последовательная обработка событий Приложения GUI ориентированы на обработку небольших событий, таких как щелчок мыши, нажатие клавиши или истечение времени таймера. Событие представляет собой некоторый вид задачи; механизм обработки событий, предоставляемый AWT и Swing, структурно похож на механизм Executor Поскольку существует только один поток для обработки задач GUI, они обрабатываются последовательно - одна задача завершается до начала выполнения следующей, и никакие две задачи не перекрываются. Это знание упрощает написание кода задачи – вам не нужно беспокоиться о вмешательстве других задач. Недостатком последовательной обработки задач является то, что если выполнение одной задачи занимает много времени, другие задачи должны ждать ее завершения. Если эти другие задачи ответственны за реакцию на ввод данных пользователем или обеспечение визуальной обратной связи, приложение будет казаться замороженным. Если в потоке событий выполняется длительная задача, пользователь даже не сможет нажать кнопку "Отмена", поскольку слушатель кнопки отмены не будет вызван до завершения длительной задачи. Поэтому задачи, выполняемые в потоке событий, должны как можно быстрее возвращать управление потоку событий. Чтобы запустить длительную задачу, такую как проверка орфографии большого документа, поиск в файловой системе или получение ресурса по сети, необходимо запустить эту задачу в другом потоке, чтобы элемент управления мог быстро вернуться в поток событий. Для обновления состояния индикатора выполнения во время обработки длительной задачи или обеспечения визуальной обратной связи после ее завершения, необходимо вновь выполнить код в потоке событий. Всё это ведёт к быстрому усложнению программы. 9.1.2 Ограничение потока в Swing Все компоненты Swing (такие как JButton и JTable ) и объекты модели данных (такие как TableModel и TreeModel ) ограничены потоком событий, таким образом, любой код, который обращается к этим объектам, должен работать в потоке событий. Объекты GUI хранятся согласованно без использования синхронизации, но с помощью ограничения потока. Преимуществом такого подхода является то, что задачи, выполняемые в потоке событий, не должны беспокоиться о синхронизации, когда получают доступ к объектам презентации; недостатком такого подхода является то, что вы вообще не можете получить доступ к объектам презентации извне потока событий. Правило однопоточности Swing: компоненты и модели Swing должны создаваться, изменяться и запрашиваться только из потока диспетчеризации событий. Как и во всех правилах, имеется несколько исключений. Небольшое количество методов Swing может быть безопасно вызвано из любого потока; они явно определены в Javadoc как потокобезопасные. Другие исключения из правила однопоточности включают: • Метод SwingUtilities isEventDispatchThread , определяющий, является ли текущий поток потоком событий; • Метод SwingUtilities invokeLater , планирующий выполнение экземпляра Runnable в потоке событий (вызываемый из любого потока); • Метод SwingUtilities invokeAndWait , планирующий задачу выполнения экземпляра Runnable в потоке событий и блокирующий текущий поток до её завершения (вызывается только не из потоков GUI); • Методы, помещающие запросы на перерисовку или повторную проверку в очередь событий (вызываются из любого потока); и • Методы, добавляющие и удаляющие слушателей (можно вызывать из любого потока, но слушатели всегда будут вызываться в потоке событий). Методы invokeLater и invokeAndWait функционируют подобно механизму Executor . На практике, достаточно просто реализовать связанные с потоками методы класса SwingUtilities , используя однопоточную реализацию Executor , как показано в листинге 9.1. public class SwingUtilities { private static final ExecutorService exec = Executors.newSingleThreadExecutor(new SwingThreadFactory()); private static volatile Thread swingThread; private static class SwingThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { swingThread = new Thread(r); return swingThread; } } public static boolean isEventDispatchThread() { return Thread.currentThread() == swingThread; } public static void invokeLater(Runnable task) { exec.execute(task); } public static void invokeAndWait(Runnable task) throws InterruptedException, InvocationTargetException { Future f = exec.submit(task); try { f.get(); } catch (ExecutionException e) { throw new InvocationTargetException(e); } } } Листинг 9.1 Реализация класса SwingUtilities с использованием экземпляра Executor Описанное выше не отражает того, как класс SwingUtilities реализован фактически, поскольку фреймворк Swing предшествует фреймворку Executor , но, вероятно отражает, как он мог бы быть построен, если бы Swing реализовывался сегодня. Поток событий Swing можно рассматривать как однопоточную реализацию Executor , обрабатывающую задачи из очереди событий. Как и в случае пулов потоков, рабочий поток иногда умирает и заменяется новым, но это должно происходить прозрачно для задач. Последовательное однопоточное выполнение является разумной политикой выполнения, когда задачи кратковременные, предсказуемость планирования не имеет значения или крайне важно, чтобы задачи не выполнялись параллельно. Класс GuiExecutor из листинга 9.2 представляет собой реализацию Executor , делегирующую задачи на выполнение классу SwingUtilities . Подобный подход мог бы быть реализован и в терминах других GUI-фреймворков; например, фреймворк SWT предоставляет метод Display.asyncExec , подобный методу invokeLater фреймворка Swing. public class GuiExecutor extends AbstractExecutorService { // Singletons have a private constructor and a public factory private static final GuiExecutor instance = new GuiExecutor(); private GuiExecutor() { } public static GuiExecutor instance() { return instance; } public void execute(Runnable r) { if (SwingUtilities.isEventDispatchThread()) r.run(); else SwingUtilities.invokeLater(r); } // Plus trivial implementations of lifecycle methods } Листинг 9.2 Реализация Executor построенная на основе класса SwingUtilities 9.2 Кратковременные задачи GUI В приложении GUI события происходят в потоке событий и всплывают до уровня слушателей, предоставляемых приложением, которые, вероятно, выполнят некоторые вычисления, влияющие на объекты уровня представления. Для простых кратковременных (short-running) задач, действие может целиком оставаться в потоке событий; для долговременных (longer-running) задач, часть обработки должна быть выгружена в отдельный поток. В простом случае, ограничение объектов представления потоком событий вполне естественно. В листинге 9.3 создаётся кнопка, цвет которой в момент нажатия изменяется случайным образом. Когда пользователь нажимает на кнопку, инструментарий доставляет экземпляр события ActionEvent всем зарегистрированным в потоке событий слушателям действий (action listeners). В ответ слушатель действий выбирает новый цвет и изменяет цвет фона кнопки. final Random random = new Random(); final JButton button = new JButton("Change Color"); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { button.setBackground(new Color(random.nextInt())); } }); Листинг 9.3 Простой слушатель событий Таким образом, событие берёт своё начало в инструментарии GUI и доставляется приложению, а приложение изменяет GUI в ответ на действие пользователя. Как показано на рис. 9.1, элемент управления никогда не покидает поток событий. Рисунок 9.1 Контроль управления в случае простого клика по кнопке Этот тривиальный пример характеризует большинство взаимодействий между GUI-приложениями и инструментариями GUI. До тех пор, пока задачи кратковременны и имеют доступ только к объектам GUI (или другим ограниченным потоком или потокобезопасным объектам приложений), вы можете почти полностью игнорировать проблемы с потоками и делать все из потока событий, и всё будет происходить правильно. Несколько более сложная версия того же сценария, иллюстрируемая на рис. 9.2, предполагает использование формальной модели данных, такой как TableModel или TreeModel . Swing разделяет большинство визуальных компонентов на два объекта: модель (model) и представление (view). Отображаемые данные находятся в модели, а правила, регулирующие их отображение, - в представлении. Рисунок 9.2 Контроль управления в случае разделения объектов модели и представления Объекты модели могут инициировать события, указывающие на изменение данных в модели, и представления подписываются на эти события. Когда представление получает событие, указывающее, что данные модели, возможно, изменились, оно запрашивает у модели новые данные и обновляет отображение. Таким образом, в слушателе кнопки, изменяющей содержимое таблицы, слушатель действий обновит модель и вызовет один из методов fireXxx , который, в свою очередь, вызовет слушателя табличной модели представления, который обновит представление. И вновь, элемент управления никогда не покидает поток событий. EDT mouse click action event action listener set color EDT mouse click action event action listener update table model table changed event table listener update table view (Методы fireXxx модели данных Swing всегда напрямую вызывают слушателей модели, вместо того, чтобы передать новое событие в очередь событий, таким образом, методы fireXxx должны вызывать только в потоке событий.) 9.3 Долговременные задачи GUI Если бы все задачи были кратковременными (и у приложения не было бы существенной части кода без GUI), то всё приложение могло бы работать в потоке событий, и вам в принципе не пришлось бы обращать внимание на потоки. Однако сложные приложения с графическим интерфейсом могут выполнять задачи, выполнение которых может занять больше времени, чем пользователь готов ожидать, например, проверка орфографии, фоновая компиляция или извлечение удаленных ресурсов. Эти задачи должны выполняться в другом потоке, чтобы графический интерфейс оставался отзывчивым во время их выполнения. Фреймворк Swing упрощает выполнение задачи в потоке событий, но (до Java версии 6) не предоставляет механизма, оказывающего помощь потокам GUI в выполнении кода в других потоках. В принципе, в этом вопросе мы можем обойтись и без помощи со стороны Swing: мы можем создать свою собственную реализацию Executor для обработки долговременных задач. Кэшированный пул потоков является хорошим выбором для долговременных задач; достаточно редко приложения GUI инициируют большое количество долговременных задач, таким образом, существующий риск роста пула потоков без ограничений невелик. Мы начнём с простых задач, которые не поддерживают отмену или индикацию прогресса и не обновляют GUI по завершению выполнения, а затем будем постепенно, по одной, добавлять эти функции. В листинге 9.4 показан слушатель действий, ограниченный визуальным компонентом, который отправляет долговременную задачу экземпляру Executor . Несмотря на два уровня внутренних классов, инициировать исполнение задачи с использованием приведённого выше подхода, при наличии задачи GUI, довольно просто: слушатель действий пользовательского интерфейса вызывается в потоке событий и отправляет экземпляр Runnable на выполнение в пул потоков. ExecutorService backgroundExec = Executors.newCachedThreadPool(); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { backgroundExec.execute(new Runnable() { public void run() { doBigComputation(); } }); }}); Листинг 9.4 Привязка долговременной задачи к визуальному компоненту Этот пример извлекает долговременную задачу из потока событий способом "воспламенить и забыть", что, вероятно, не очень полезно. Обычно, при выполнении долговременной задачи, возникает визуальная обратная связь. Но вы не можете получить доступ к презентационным объектам из фонового потока, поэтому, после завершения выполнения, задача должна отправить другую задачу для запуска в потоке событий, с целью обновления состояния пользовательского интерфейса. В листинге 9.5 иллюстрируется очевидный способ реализации такого подхода, начавший постепенно усложняться; теперь мы имеем уже три уровня внутренних классов. Слушатель действий сначала затемняет кнопку и устанавливает метку, указывающую, что выполняется вычисление, а затем отправляет задачу фоновому исполнителю. Когда текущая задача завершается, она помещает в очередь на выполнение в потоке событий другую задачу, которая повторно включает кнопку и восстанавливает текст метки. button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { button.setEnabled(false); label.setText("busy"); backgroundExec.execute(new Runnable() { public void run() { try { doBigComputation(); } finally { GuiExecutor.instance().execute(new Runnable() { public void run() { button.setEnabled(true); label.setText("idle"); } }); } } }); } }); Листинг 9.5 Долговременные пользовательские задачи с обратной связью Задача, запускаемая при нажатии кнопки, состоит из трех последовательных подзадач, выполнение которых чередуется между потоком событий и фоновым потоком. Первая подзадача обновляет пользовательский интерфейс, показывая, что началось выполнение длительной операции, и запускает вторую подзадачу в фоновом потоке. По завершении вторая подзадача помещает третью подзадачу в очередь потока событий для повторного выполнения, целью обновления пользовательского интерфейса, для отражения того, что операция завершена. Этот вид “скачкообразной перестройки потока” типичен для обработки длительных задач в приложениях GUI. 9.3.1 Отмена Любая задача, запуск которой на выполнение в другом потоке занимает достаточно много времени, вероятно, также потребует достаточно много времени на её отмену пользователем. Вы можете реализовать отмену напрямую, используя прерывание потока, но гораздо проще использовать интерфейс Future , который был специально разработан для управления отменяемыми задачами. При вызове метода cancel экземпляра Future , с параметром mayInterruptIfRunning установленным в true , реализация Future прерывает поток, выполняющий задачу, если он выполняется в данный момент. Если ваша задача написана отзывчивой на прерывание, в случае инициации процесса отмены, она может вернуть управление раньше. В листинге 9.6 иллюстрируется задача, которая опрашивает статус прерывания потока и при прерывании возвращает управление раньше. Future> runningTask = null; // thread-confined startButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (runningTask == null) { runningTask = backgroundExec.submit(new Runnable() { public void run() { while (moreWork()) { if (Thread.currentThread().isInterrupted()) { cleanUpPartialWork(); break; } doSomeWork(); } } }); }; }}); cancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { if (runningTask != null) runningTask.cancel(true); }}); Листинг 9.6 Отмена долговременной задачи Поскольку переменная runningTask ограничена потоком событий, при установке или проверке значения синхронизация не требуется, а слушатель кнопки запуска гарантирует, что одновременно выполняется только одна фоновая задача. Однако лучше получать уведомления о завершении задачи, чтобы, например, можно было отключить кнопку отмены. Мы рассмотрим этот подход в следующем разделе. 9.3.2 Индикатор прогресса и завершения Использование экземпляра Future для представления долговременной задачи значительно упрощает реализацию механизма отмены. Класс FutureTask имеет хук done , что так же облегчает уведомление о завершении. После завершения фонового выполнения экземпляра Callable , вызывается метод done . Выполняя метод done в потоке событий, по завершении задачи, мы можем построить класс BackgroundTask , предоставляющий хук onCompletion , который вызывается в потоке событий, как показано в листинге 9.7. abstract class BackgroundTask private class Computation extends FutureTask BackgroundTask.this.compute(); } }); } protected final void done() { GuiExecutor.instance().execute(new Runnable() { public void run() { V value = null; Throwable thrown = null; boolean cancelled = false; try { value = get(); } catch (ExecutionException e) { thrown = e.getCause(); } catch (CancellationException e) { cancelled = true; } catch (InterruptedException consumed) { } finally { onCompletion(value, thrown, cancelled); } }; }); } } protected void setProgress(final int current, final int max) { GuiExecutor.instance().execute(new Runnable() { public void run() { onProgress(current, max); } }); } // Called in the background thread protected abstract V compute() throws Exception; // Called in the event thread protected void onCompletion(V result, Throwable exception, boolean cancelled) { } protected void onProgress(int current, int max) { } // Other Future methods forwarded to computation } Листинг 9.7 Класс фоновой задачи, поддерживающий механизм отмены, уведомление о завершении задачи и уведомление о ходе выполнения Класс BackgroundTask также поддерживает индикацию хода выполнения. Метод compute может вызывать метод setProgress , указывая прогресс в числовом выражении. Хук onProgress вызывающийся из потока событий, может обновить пользовательский интерфейс для визуального отображения состояния прогресса. Для реализации абстрактного класса BackgroundTask , достаточно реализовать метод compute , вызывающийся в фоновом потоке. У вас также есть возможность переопределения хуков onCompletion и onProgress , которые вызываются в потоке событий. Класс BackgroundTask базирующийся на классе FutureTask , также упрощает отмену. Вместо того, чтобы опрашивать статус прерывания потока, метод compute может вызвать метод Future.isCancelled . В Листинге 9.8 демонстрируется пример из листинга 9.6, переписанный с использованием класса BackgroundTask startButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { class CancelListener implements ActionListener { BackgroundTask> task; public void actionPerformed(ActionEvent event) { if (task != null) task.cancel(true); } } final CancelListener listener = new CancelListener(); listener.task = new BackgroundTask } public void onCompletion(boolean cancelled, String s, Throwable exception) { cancelButton.removeActionListener(listener); label.setText("done"); } }; cancelButton.addActionListener(listener); backgroundExec.execute(listener.task); } }); Листинг 9.8 Инициация долговременной, отменяемой задачи с помощью класса BackgroundTask 9.3.3 Класс SwingWorker Мы построили простой фреймворк, с использованием класса FutureTask и интерфейса Executor , для выполнения долговременных задач в фоновых потоках, без ущерба для отзывчивости GUI. Эти методы могут быть применены к любому однопоточному фреймворку GUI, а не только Swing. В Swing, многие ранее разработанные функции, предоставляются классом SwingWorker , включая отмену, уведомление о завершении и индикацию хода выполнения. Различные версии SwingWorker были опубликованы в The Swing Connection и The Java Tutorial, а их обновленная версия была включена в Java 6. 9.4 Совместно используемые модели данных Объекты представления Swing, включая объекты модели данных, такие как TableModel или TreeModel , ограничены потоком событий. В простых программах с GUI все изменяемое состояние хранится в объектах презентации, и единственный поток, кроме потока событий, является основным потоком. В этих программах легко применить правило однопоточности: не обращаться к компонентам модели данных или представления из основного потока. Более сложные программы могут использовать другие потоки для перемещения данных в постоянное хранилище или из него, например, в файловую систему или базу в данных, чтобы не ухудшать скорость отклика. В простейшем случае, данные в модели данных вводятся пользователем или загружаются статически из файла или другого источника данных при запуске приложения, и в этом случае к данным никогда не обращаются потоки, отличные от потока событий. Но иногда объект модели представления (presentation model object) выступает только представлением (view) другого источника данных, например базы данных, файловой системы или удаленной службы. В этом случае более чем один поток может получить доступ к данным в момент входа или в момент выхода из приложения. Например, можно отобразить содержимое удаленной файловой системы с помощью древовидного элемента управления. Вы не хотели бы перечислять всю файловую систему, прежде чем вы смогли бы отобразить древовидный элемент управления - это займет слишком много времени и памяти. Вместо этого, дерево можно заполнять по мере раскрытия узлов. Перечисление даже одного каталога на удаленном томе может занять довольно много времени, поэтому может потребоваться выполнить перечисление в фоновой задаче. После завершения фоновой задачи, необходимо каким-то образом поместить данные в модель дерева. Это можно сделать с помощью потокобезопасной модели дерева, “проталкивая” данные из фоновой задачи в поток событий, путем публикации задачи с помощью метода invokeLater или путем опроса состояния потоком событий, чтобы увидеть, доступны ли данные. 9.4.1 Потокобезопасные модели данных До тех пор, пока блокировка не оказывает чрезмерного влияния на отзывчивость, проблему нескольких потоков, работающих с данными, можно решить с помощью потокобезопасной модели данных. Если модель данных поддерживает разбиение на небольшие параллельные операции, поток событий и фоновые потоки должны иметь возможность совместно использовать её без проблем с откликом. Например, класс DelegatingVehicleTracker использует базовый класс ConcurrentHashMap , операции извлечения данных которого обеспечивают высокую степень параллелизма. Недостатком является то, что он не предлагает согласованный моментальный снимок данных (snapshot of the data), который может требоваться или не требоваться. Потокобезопасные модели данных также должны генерировать события при обновлении модели, чтобы представления могли обновляться при изменении данных. Иногда возможно получить потокобезопасность, согласованность и хорошую отзывчивость с помощью версионной модели данных (versioned data model such), такой как класс CopyOnWriteArrayList [CPJ 2.2.3.3]. Когда вы захватываете итератор для коллекции скопировать-и-записать, итератор проходит по коллекции в том виде, в котором она существовала на момент создания итератора. Однако коллекции скопировать-и-записать обеспечивают хорошую производительность только в том случае, если количество проходов значительно превышает количество модификаций, что, вероятно, не будет иметь место, например, в приложении отслеживания транспортных средств. Более специализированные версионные структуры данных могут избежать этого ограничения, но создание версионных структур данных, которые обеспечивают эффективный параллельный доступ и не сохраняют старые версии данных дольше, чем это необходимо, непросто, и поэтому такой вариант следует рассматривать только тогда, когда другие подходы непрактичны. 9.4.2 Разделение моделей данных С точки зрения GUI, классы табличной модели Swing, такие как TableModel и TreeModel , являются официальным репозиторием для отображаемых данных. Однако объекты модели часто сами являются "представлениями" других объектов, управляемых приложением. О программе, которая имеет как домен представления (presentation-domain), так и домена приложения (applicationdomain), говорят, что она спроектирована в виде разделённой модели (split-model, Fowler, 2005). В дизайне с разделённой моделью, модель представления ограничена потоком событий и другой моделью - совместно используемой моделью (shared model), которая является потокобезопасной и доступ к ней может осуществляться как из потока событий, так и из потоков приложения. Модель представления регистрирует слушателя в совместно используемой модели, чтобы получать уведомления об обновлениях. Затем модель представления можно обновить из совместно используемой модели путем встраивания моментального снимка соответствующего состояния в сообщение об обновлении или путем получения моделью представления данных непосредственно из совместно используемой модели, при получении события обновления. Подход с моментальными снимками прост, но имеет ограничения. Он хорошо работает, когда модель данных мала, обновления не слишком часты, и структура обеих моделей подобна. Если модель данных имеет большой размер или обновления происходят очень часто, или если одна или обе стороны разделения содержат информацию, которая не видна другой стороне, более эффективным подходом может быть отправка инкрементных обновлений вместо полных снимков. Этот подход приводит к эффекту сериализации обновлений в совместно используемой модели и воссоздании их в потоке событий для модели представления. Другим преимуществом инкрементных обновлений является то, что более детальная информация о том, что изменилось, может улучшить качество восприятия информации с дисплея - если движется только один автомобиль, нам не нужно перекрашивать весь дисплей, а только те регионы, что были затронуты обновлениями. Рассмотрите возможность проектирования с разделённой моделью, когда модель данных должна совместно использоваться несколькими потоками, и реализация потокобезопасной модели данных нецелесообразна по причинам блокировки, согласованности или сложности. 9.5 Другие формы однопоточных подсистем Ограничение потока не ограничивается GUI: его можно использовать всякий раз, когда объект реализуется как однопоточная подсистема. Иногда ограничение потока принудительно вводится разработчиком по причинам, которые не имеют ничего общего с желанием избежать синхронизации или взаимоблокировок. Например, некоторые нативные (native) библиотеки требуют, чтобы весь доступ к библиотеке, даже загрузка библиотеки с помощью вызова System.loadLibrary , осуществлялся из одного и того же потока. Заимствуя подход, принятый в GUI фреймворках, для доступа к нативной библиотеке вы можете легко создать выделенный поток или однопоточный исполнитель и предоставить прокси-объект, который будет перехватывать вызовы, поступающие к ограниченному потоком объекту, и отправлять их в качестве задач выделенному потоку. Для упрощения реализации такого подхода, экземпляр Future и метод newSingleThreadExecutor работают в связке; прокси-метод может отправить задачу и немедленно вызвать метод Future.get для ожидания результата. (Если ограниченный потоком класс реализует интерфейс, можно автоматизировать процесс отправки экземпляра Callable фоновому исполнителю потока и ожидать получение результата с помощью динамического прокси.) 9.6 Итоги Фреймворки GUI почти всегда реализуются как однопоточные подсистемы, в которых весь связанный с представлением код выполняется в качестве задач в потоке событий. Поскольку существует только один поток событий, долговременные задачи могут существенно снижать скорость отклика и поэтому должны выполняться в фоновых потоках. Вспомогательные классы, такой как SwingWorker или приведённый в главе класс BackgroundTask , обеспечивающие поддержку отмены, индикации прогресса и индикации завершения, могут упростить разработку долговременных задач, имеющих как GUI, так и компоненты без GUI. Часть 3 III Живучесть, производительность и тестирование |