При участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
Скачать 4.97 Mb.
|
Глава 1 Введение Написать корректную программу трудно. Написать многопоточную корректную программу ещё труднее. В параллельной программе может произойти огромное количество вещей, которые выполняться не так, как ожидалось, хотя, вполне корректно, сработают в последовательной программе. Так почему мы связываемся с многопоточностью? Потоки являются неизбежной частью языка Java, и они могут существенно упростить разработку комплексных систем путём превращения сложного асинхронного кода в более простой прямолинейный код. В дополнение, потоки являются простейшим способом использовать вычислительную мощь многопроцессорных систем. По мере увеличения количества процессоров, важность эффективного использоваться параллелизма будет только возрастать. 1.1 Очень краткая история параллелизма В давнем прошлом, компьютеры не имели операционных систем; они выполняли единственную программу от начала и до конца, и программа имела прямой доступ ко всем ресурсам машины. Писать программы, которые работали на голом железе, было не единственной сложностью, другой была возможность запуска за раз только одной программы, что вызывало неэффективное использование дорогостоящих и ограниченных ресурсов компьютера. Эволюционировав, операционные системы позволили запускать более одной программы за раз, запуская программы в индивидуальных процессах: изолированных, независимо выполняемых программах, для которых операционная система выделает ресурсы, такие как память, указатели на файлы и ограничения системы безопасности. Также, если необходимо, процессы могут взаимодействовать с другими процессами через различные механизмы коммуникации: сокеты, обработчики сигналов, разделяемую память, семафоры и файлы. Несколько мотивирующих факторов привели к разработке операционных систем, которые позволяли одновременно выполнять несколько программ: Использование ресурсов. Программы иногда вынуждены ожидать завершения внешних операций, таких как ввод или вывод, и пока длится ожидание, не могут выполнять полезную работу. Гораздо эффективнее использовать время ожидания, позволив выполняться другой программе. Справедливость. Множество ресурсов и программ могут иметь эквивалентные запросы к ресурсам машины. Предпочтительно позволить им разделять ресурсы компьютера на равно-порционные куски времени, чем ожидать завершения одной программы и запускать другую Удобство. Часто проще и предпочтительнее написать несколько программ, каждая из которых выполняет одну задачу и имеет возможность координироваться с другими (если это необходимо), чем написать одну программу, которая выполняет сразу все задачи. В ранних системах с разделением времени, каждый процесс являлся виртуальным Фон-Неймановским компьютером, он имел пространство в памяти для совместного хранения инструкций и данных, выполнялись инструкции последовательно, в соответствии с семантикой языка машины, и взаимодействовали с внешним миром через набор примитивов ввода/вывода операционной системы. Для каждой выполняемой инструкции была четко определена «следующая инструкция» и контроль потока выполнения программы осуществлялся строго в соответствии с правилами набора инструкций. Почти все широко используемые сегодня языки программирования следуют этой последовательной программной модели, где спецификация языка программирования чётко определяет, «что последует далее» после выполнения некоторой команды. Модель последовательного программирования интуитивна и естественна, и, фактически повторяет модель работы человека: делать по одной вещи в один момент времени, в основном – последовательно. Встать с постели, облачится в халат, спустится вниз и приготовить чай. Как и в языках программирования, каждое из действий реального мира является абстракцией для последовательности небольших действий - открыть шкаф, выбрать сорт чая, отмерить некоторое количество в чайник, проверить, достаточно ли в чайнике воды, если нет, то долить немного воды в него, поставить чайник на плиту, ожидать пока вода закипит, и так далее. Этот последний шаг – ожидание, пока закипит вода – предполагает некоторую степень асинхронности (asynchrony). Пока вода нагревается, вы можете выбрать, что делать – просто подождать, или выполнить другую задачу в это время, такую как начать жарить тосты (другая асинхронная задача) или почитать новости, учитывая, что в скором времени вашего внимания потребует чайник. Производители чайников и тостеров знают, что их продукты часто используются в асинхронной манере, поэтому устройства подают звуковой сигнал, когда завершают свою работу. Поиск правильной балансировки последовательных и асинхронных действий часто является характеристикой эффективного человека – это также справедливо для программ. То же самое (утилизация ресурсов, справедливое распределение и удобство) что мотивировал разработку процессов, также мотивировало разработку потоков (threads). Потоки позволяют нескольким потокам управления программным потоком (control flow) сосуществовать в процессе. Они разделяют общие ресурсы процесса, такие как память и указатели на файлы, о каждый поток имеет свой собственный программный счётчик, стек, и локальные переменные. Потоки также предоставляют естественный механизм декомпозиции для использования аппаратного параллелизма в многопроцессорных системах; множество потоков внутри программы могут быть запущены одновременно на множество ЦПУ. Потоки иногда называют легковесными процессами, и большинство современных операционных систем рассматривают потоки, не процессы, как простейшие единицы планирования. В отсутствие явной координации, потоки выполняются одновременно и асинхронно относительно друг друга. С момента обращения потоков к общей разделяемой памяти собственного процесса, все потоки внутри процесса имеют доступ к переменным и выделяют объекты из кучи (heap), которая позволяет организовать более изящное совместное использование данных (data sharing), чем механизмы межпроцессного взаимодействия. Но без явной синхронизации для координации доступа к совместно используемым данным, поток может изменить переменную, пока другой поток находится в процессе использования переменной (in the middle of using), что приведёт к непредсказуемым результатам. 1.2 Преимущества потоков Когда потоки используются должным образом, они могут снизить сложность разработки и стоимость сопровождения кода и повысить общую производительность приложения. Потоки упрощают формирование модели поведения и взаимодействия человека, путём превращения асинхронных рабочих процессов в большей части последовательные. В другом случае они могут превратить запутанный код в прямолинейный, который проще писать, читать и сопровождать. Потоки полезны в приложениях GUI для повышения отзывчивости пользовательского интерфейса, и в серверных приложениях для улучшения использования ресурсов и повышения пропускной способности. Они также упрощают реализация JVM – сборщик мусора обычно запускается в одном или более выделенных потоках. Наиболее нетривиальные приложения на Java при создании в большей степени полагаются на потоки. 1.2.1 Использование нескольких процессоров Ранее многопроцессорные системы использовались редко и стоили дорого, как правило, они находились только в больших ЦОД-ах (data centers) или применялись в научном оборудовании. Сегодня они дёшевы и многочисленны, даже малые сервера или среднего уровня рабочие станции часто имеют множество процессоров. Эта тенденция будет только возрастать; так как всё сложнее становится увеличивать тактовую частоту, производители процессоров будут вместо этого располагать большее количество ядер на одном чипе. Все ведущие производители чипов начали этот переход, и мы уже видим машины с очень большим количеством процессоров. Поскольку основной единицей планирования является поток, программа только с одним потоком может запускаться не более чем на одном процессоре за раз. На двух процессорной системе, однопоточная программа получит доступ к половине доступных ресурсов ЦПУ; на 100-процессорной системе она откажется от 99% доступных ресурсов. С другой стороны, программы с множеством активных потоков могут одновременно выполняться на множестве процессоров. Будучи должным образом спроектирована, многопоточная программа может увеличить пропускную способность путём более эффективного использования доступных ресурсов процессора. Использование многопоточности может также помочь достигнуть большей пропускной способности на однопроцессорной машине. Если программа однопоточная, процессор просто ожидает (idle) завершения синхронных операций ввода/вывода. Если программа многопоточная, другой поток может все ещё выполняться, пока первый поток ожидает завершения операций ввода/вывода, позволяя приложению продолжать работу во время блокировки ввода/вывода. (Это похоже на чтение новостей, во время ожидания закипания воды в чайнике, вместо того, чтобы ждать пока вода закипит, прежде чем начать читать.) 1.2.2 Упрощение моделирования Часто вам проще управлять временем, когда вы выполняете задачу только одного вида (правите эти 12 ошибок(bugs)), чем, когда вы имеете несколько их (исправляете эти ошибки, проводите интервью с кандидатами на замену системного администратора, оценивание производительность вашей команды, и создаёте слайды для презентации на следующей неделе). Когда вы заняты задачей только одного вида, вы можете начать сверху вороха и взять её в работу, и так до тех пор, пока вся куча не будет разобрана; (или вы); вам нет необходимости затрачивать мыслительную энергию для понимания, какая работа будет следующей. С другой стороны, управление множеством приоритетов и сроков и переключение от задачи к задаче обычно вносит накладные расходы. Также это справедливо и для программного обеспечения: программа, которая последовательно выполняет только один тип задач, проще в написании, менее подвержена ошибкам, и проще в тестировании, чем одна, управляющая множеством различных типов задач за раз. Назначение потоков каждому типу задач, или даже элементам, в симуляции создаёт иллюзию последовательности и изолированности логики домена (domain logic) от деталей планирования, чередования операций, асинхронного ввода/вывода, и ожидания ресурсов. Сложные, асинхронные рабочие процессы (workflows) могут быть разбиты на конечное количество простых, синхронных рабочих процессов, каждый из которых запущен в отдельном потоке, взаимодействующих друг с другом только в определённых точках синхронизации (synchronization points). Эти преимущества часто эксплуатируются фрэймворками, такими как сервлеты или RMI (удалённый вызов процедур). Фреймворки берут на себя детали управления запросами, создания потоков, балансировкой нагрузки, диспетчеризуют порции запросов обрабатываемых соответствующими компонентами приложения в соответствующих точках рабочего процесса. Тем, кто пишет сервлеты, нет необходимости беспокоится о том, как много запросов будет обработано в один момент времени или о блоке ввода и вывода сокета; когда сервисный метод сервлета (service method) вызывается в ответ на веб-запрос, он может обрабатывать запрос асинхронно, как если бы это была однопоточная программа. Это может упростить разработку компонентов и снизить кривую обучения для использования фрэймворков. 1.2.3 Упрощённая обработка асинхронных событий Серверное приложение, принимающее подключения сокетов от множества удалённых клиентов может быть проще в разработке, когда под каждое подключение выделяется собственный поток и в нём позволяется использовать синхронный ввод/вывод. Если приложение приступает к чтению из сокета, когда данные недоступны, блоки будут считываться до тех пор, пока данные не станут доступны. В однопоточном приложении это значит, что приостановится не только обработка соответствующего запроса, но будет приостановлена обработка всех запросов, пока один поток заблокирован. Для обхода этой проблемы, однопоточные серверные приложения вынуждены использовать неблокирующий ввод/вывод, который намного сложнее и более подвержен ошибкам, чем синхронные операции ввода/вывода. Однако, если каждый запрос имеет свой собственный поток, тогда блокировка не оказывает влияние на обработку других запросов. Исторически так сложилось, что операционные системы располагали относительно низким лимитом на количество потоков, которые мог создать процесс, всего несколько сотен (или даже меньше). В результате операционные системы разработали эффективные средства для мультиплексированного ввода/вывода, такие как системные вызовы UNIX select и poll, и для доступа к этим средствам библиотекой классов Java был добавлен набор пакетов (java.nio) для неблокирующего ввода/вывода. Тем не менее, операционные системы стали поддерживать значительно большее количество потоков, таким образом, предоставив возможность практической реализации модели отдельный-поток-для- каждого-клиента, даже для большого количества клиентов для некоторых платформ 2 1.2.4 Более отзывчивый пользовательский интерфейс Приложения GUI ранее были однопоточными, что приводило к тому, что-либо приходилось часто опрашивать код на возникновение входящих событий (которые беспорядочны и назойливы), либо выполнять весь код приложения косвенно, через “основной цикл событий (main event loop)”. Если код, вызванный из основного цикла событий, выполнялся слишком долго, то пользовательский интерфейс “замерзал” в ожидании завершения его работы, потому что последующие события GUI не могут быть обработаны до тех пор, пока не будет возвращено управление основному циклу событий. Современные фреймворки GUI 3 , такие как AWT или Swing toolkits, заменили основной цикл событий механизмом рассылки событий (event dispatch thread, edt). Когда возникает событие пользовательского интерфейса, например, такое как нажатие кнопки, в потоке событий будут вызваны объявленные в приложении обработчики. Большинство фрэймворков GUI имеют однопоточные подсистемы, так что основной цикл событий всё ещё присутствует, но он запускается в собственном потоке под управлением инструментария GUI, а не приложения. Если в потоке событий выполняются кратковременные задачи, интерфейс остаётся отзывчивым, поскольку поток событий всегда способен обрабатывать действия пользователя достаточно быстро. Однако, обработка долго выполняющихся задач в потоке событий, таких как проверка орфографии, в большом документе или получение ресурсов по сети, ухудшает отзывчивость. Если пользователь выполняет какие-то действия, пока выполняется такая задача, появится большая задержка до того момента, как поток событий сможет обработать их или даже просто узнать о них. Сгущая краски, отметим, что пользовательский интерфейс не только становится недоступен, но также невозможно нажать кнопку отмены задачи, даже если UI имеет таковую, потому что поток событий занят и не может обработать событие нажатия кнопки до завершения длинной задачи! Однако, если вместо этого, долго работающая задача выполняется в отдельном потоке, поток событий остаётся свободным для обработки событий UI, делая UI более отзывчивым. 1.3 Риски, которые несут потоки Встроенная в Java поддержка потоков — это обоюдоострый меч. Хотя это упрощает разработку многопоточных приложений, предоставляя поддержку со стороны языка и библиотек и формальную кроссплатформенную модель памяти (именно эта формальная кроссплатформенная модель памяти Java делает возможной разработку однажды написанных, запускающихся где угодно многопоточных приложений), это также поднимает планку уровня разработчиков, потому что всё больше количество программ будет использовать потоки. Когда 2 Пакет потоков NPTL, сейчас являющийся частью большинства дистрибутивов Linux, был спроектирован для поддержки сотен тысяч потоков. Неблокирующий ввод/вывод имеет свои преимущества, но улучшенная поддержка ОС для потоков означает, что существует немного ситуаций, для которых необходимо его использование. 3 Для тех лет это было круто, по современным меркам – ад. потоки были вещью скорее эзотерической, параллельность была “подвинутой” темой; сейчас общий тренд таков, что разработчики должны быть осведомлены о безопасном использовании потоков. 1.3.1 Угрозы безопасности Потокобезопасность может быть неожиданно тонким понятием: из-за отсутствия достаточной синхронизации, порядок операций во множестве потоков непредсказуем и иногда случаются сюрпризы. Класс UnsafeSequence в листинге 1.1, который должен генерировать последовательность уникальных целых чисел, предлагает простую иллюстрацию того, как чередование действий в множестве потоков может к нежелательным результатам. Он ведёт себя корректно в пределах однопоточного окружения, но в многопоточном окружении всё иначе. @NotThreadSafe public class UnsafeSequence { private int value; /* Returns a unique value. */ public int getNext() { return value++; } } Листинг 1.1 Непотокобезопасный генератор последовательностей. Проблема с UnsafeSequence возникает в неудачный момент времени, два потока могут вызвать getNext и получить некоторое значение. На рис. 1.1 показано, как это может произойти. Нотация инкремента, someVariable++, может казаться единой операцией, но это фактически три раздельных операции: чтения значения, добавления единички к нему, и запись нового значения в переменную. Так как операции в множестве потоков могут произвольно чередоваться во время выполнения, вполне возможно для двух потоков прочитать значения в один момент времени, и затем добавить по единичке к ним. В результате один и тот же порядковый номер возвращается из нескольких вызовов в разных потоках. Рис. 1.1 Неудачное выполнение UnsafeSequence.getNext. Диаграммы на рис. 1.1 показывают возможное чередование операций в различных потоках. На этих диаграммах, движется слева направо, и каждая линия представляет активность различных потоков. Эти чередующиеся value → 9 9 + 1 → 10 value → 10 value → 9 9 + 1 → 10 value → 10 A B диаграммы обычно показывают наихудший случай 4 и предназначены для того, чтобы показать опасность неверного предположения, о том, что все произойдет в строго определенном порядке. Класс UnsafeSequence использует нестандартную аннотацию: @NotThreadSafe Это одна из нескольких пользовательских аннотаций, используемых на протяжении всей книги, для документирования параллелизма свойств и членов классов. (Аналогичным образом используются другие аннотации уровня класса - @ThreadSafe и @Immutable ; см. Приложение А) Аннотации, документирующие потокобезопасность, полезны для нескольких аудиторий. Если классы аннотированы с помощью @ThreadSafe , пользователи могут с уверенностью использовать их в многопоточном окружении, со своей стороны, разработчик ставит в известность, что потокобезопасность гарантирована и должна быть сохранена, и инструменты для анализа кода могут определить возможные ошибки кодирования. Класс UnsafeSequence показывает распространённую проблему параллелизма, называемую условием гонок (race condition). Возвращает ли getNext уникальное значение при вызове из нескольких потоков, как того требует спецификация, или нет, зависит от того, как среда выполнения (runtime) чередует операции - это является нежелательным состоянием дел. Из-за того, что потоки разделяют общее адресное пространство и запускаются параллельно, они могут получать доступ и модифицировать те переменные, которые могут использовать другие потоки. Это потрясающе удобно, потому что позволяет совместно использовать данные проще, чем в любых других межпоточных механизмах коммуникации. Но, такой механизм коммуникации, также несёт в себе значительные риски: потоки могут быть дискредитированы (confused) при неожиданном изменении данных. Возможность множества потоков получить доступ и модифицировать некоторые переменные вводит элемент непоследовательности в последовательную модель программирования, что может сбивать с толку и быть сложным для понимания. Для многопоточных программ поведение должно быть предсказуемым, доступ к совместно используемым (shared) переменным должен быть правильно скоординирован так, чтобы потоки не вмешивались в работу друг друга. К счастью, Java предоставляет механизмы синхронизации для координации такого доступа. Класс UnsafeSequence может быть исправлен путём превращения метода getNext в синхронизированный (synchronized), как показано в листинге 1.2 5 , тем самым предотвращая неудачное взаимодействие как на рис. 1.1. (Подробно о том, как это работает, является темой глав 2 и 3) @ThreadSafe public class Sequence { @GuardedBy("this") private int value; public synchronized int getNext() { return value++; 4 Фактически, как мы увидим в Главе 3, наихудший случай может быть ещё сложнее, чем обычно показывается на этих диаграммах, из-за возможности переупорядочивания. 5 Аннотация @GuardedBy описывается в секции 2.4; Она документирует политику синхронизации (synchronization policy) для класса Sequence. } } Листинг 1.2. Потокобезопасный генератор последовательности В отсутствие синхронизации, компилятору, железу и среде выполнения позволяются существенные вольности в обращении с таймингами и порядком выполнения действий, такие как кэширование переменных в регистрах или в локальных кэшах процессора, где они временно (или даже постоянно) недоступны для других потоков. Эти ухищрения помогают улучшить производительность, и в общем случае желательны, но они возлагают бремя ответственности за чёткое обозначение того, где данные должны совместно использоваться между потоками, на разработчика, который должен сделать так, чтобы эти оптимизации не подрывали безопасность. В главе 16 приводятся подробные сведения о том, как именно JVM гарантирует порядок и какое влияние оказывает синхронизация на эти гарантии, но если вы последуете правилам в главах 2 и 3 , то можете свободно опустить эти низкоуровневые подробности. 1.3.2 Угрозы живучести потока Критически важно обратить внимание на вопросы потокобезопасности, когда разрабатывается параллельный код: безопасность не может быть скомпрометирована. Важность безопасности не является уникальной чертой многопоточных программ – однопоточные программы также должны заботиться о сохранении безопасности и корректности – но использование потоков представляет собой дополнительные вызовы безопасности, не представленные в однопоточных программах. Подобным образом, использование потоков порождает дополнительные формы сбоев живучести (liveness failure), которые не происходят в однопоточных программах. Пока безопасность означает “ничего плохого никогда не случается”, живучесть заботится о дополнительной цели, которая гласит “что-то хорошее когда-нибудь случается”. Сбои в работе возникают, когда активность попадает в такое состояние, что постоянно невозможен прогресс. Одной из форм сбоя в работе, который может происходить в последовательной программе, является непреднамеренное создание бесконечного цикла, из-за которого последующий код никогда не получит возможность выполнится. Использование потоков вводит дополнительные риски для живучести. Например, если поток A ожидает ресурс, который поток B удерживает эксклюзивно, и поток B никогда не освободит его, A будет ожидать вечно. Глава 10 описывает различные формы сбоев живучести потока и способы их избежать, включая взаимоблокировки (deadlock) 6 , голодание (starvation) 7 и динамическая взаимоблокировка (livelock) 8 Подобно большинству ошибок параллельного выполнения, ошибки вызывающие сбои живучести потока могут быть неуловимы, потому что зависят от времени происхождения событий (timing of events) в различных потоках, и, поэтому, не всегда проявляют себя в разработке или в тестировании. 6 Взаимная блокировка (deadlock, секция 10.1) описывает ситуацию, когда два или более потока блокируются навсегда, каждый ожидая другого. 7 Голодание (starvation, секция 10.3.1) описывает ситуацию, когда поток не может получить доступ к совместно используемым ресурсам и не может продвинуться в своём выполнении дальше. 8 Поток часто реагирует на события из другого потока. Если действие другого потока тоже является ответом на событие из другого потока, то может произойти динамическая взаимоблокировка (livelock, секция 10.3.3). Подробнее в https://urvanov.ru/2016/05/27/java-8-многопоточность/#liveness. 1.3.3 Угрозы производительности С живучестью потока связана производительность. В то время как живучесть означает, что что-то хорошее непременно произойдёт, это в конечном итоге может быть недостаточно хорошо - мы часто хотим, чтобы хорошие вещи происходили быстро. Вопросы производительности относятся к широчайшему спектру проблем, включая плохое время обслуживания, отзывчивость, пропускную способность, потребление ресурсов или масштабируемость. Как и в случае безопасности и живучести, многопоточные программы подвержены всем проблемам производительности однопоточных программ, а также прочим, добавляющимся при использовании потоков. В хорошо спроектированных параллельных приложениях использование потоков приводит к чистому приросту производительности, тем не менее, потоки приводят к большим накладным расходам во время выполнения. Переключение контекста (Context switches) – это ситуация, когда планировщик временно приостанавливает активный поток, так что другой поток может запуститься – что наиболее часто происходит в приложениях с множеством потоков и стоит довольно дорого: сохранение и восстановление контекста выполнения, потеря локальности и время ЦП затраченное на планирование потоков, вместо их запуска. Когда потоки совместно используют данные, они должны использовать механизмы синхронизации, которые могут препятствовать компилятору в проведении оптимизаций, путём сброса кэшей памяти или пометки их устаревшими, и, таким образом, создавая траффик синхронизации в шине разделяемой памяти. Все эти факторы вводят дополнительные расходы производительности; В Главе 11 описывается техника анализа и снижения стоимости. 1.4 Потоки есть везде Даже если ваша программа никогда явно не создаёт потоки, фреймворки могут создавать потоки от вашего имени, и код, вызываемый из этих потоков, должен быть потокобезопасным. Это оказывает серьёзное влияние на проектирование и реализацию приложения со стороны разработчика, так как разработка потокобезопасных классов требует больше внимания и анализа, чем разработка не потокобезопасных классов. Все приложения Java используют потоки. Когда JVM запускается, она создаёт потоки для своих внутренних нужд (сборка мусора, завершение) и главных поток для запуска метода main . Фреймворки пользовательского интерфейса AWT (Abstract Window Toolkit) и Swing создают потоки для управления событиями пользовательского интерфейса. Класс Timer создаёт потоки для выполнения отложенных задач. Компонентные фреймворки, такие как сервлеты и RMI, создают пулы потоков и вызывают методы компонентов в этих потоках. Если вы пользуетесь этими возможностями – как и многие другие разработчики – вы знакомитесь с параллельностью и безопасностью потоков, потому что эти фреймворки создают потоки и вызывают ваши компоненты из них. Хотелось бы верить, что параллельность это некоторая опциональная или продвинутая возможность языка, но реальность такова, что почти все приложения Java многопоточны и эти фреймворки не изолируют вас от необходимости правильной координации доступа к состоянию приложения (application state) 9 9 Имеются в виду члены экземпляров классов, например – поля. Когда фреймворком в приложение вводится параллельность, обычно невозможно ограничить осведомлённость о параллелизме кодом фреймворка, потому что фреймворк, по своей природе, создаёт обратные вызовы к компонентам приложения, которые, в свою очередь, получают доступ к состоянию приложения. Подобным образом, необходимость в потокобезопасности не заканчивается на компонентах, вызываемых фрэймворком – она распространяется на все пути выполнения кода, по которым происходит обращение к состоянию программы, доступные из этих компонентов. Так, потребность в потокобезопасности, становится становиться заразительной. Фреймворки вводят мнопоточность в приложение путём вызова компонентов приложения из потоков. Компоненты неизменно получают доступ к состоянию приложения, тем самым требуя, чтобы все пути выполнения кода, обращающиеся к этому состоянию, были потокобезопасными. Описанные ниже средства приводят к тому, что код приложения, вызывается из потоков, не управляемых приложением. При необходимости, потокобезопасность может начаться с этих средств, хотя редко ограничивается ими; вместо этого она струится через всё приложение. Таймер. Класс Timer является удобным механизмом для планирования отложенного запуска задач, единовременно или периодически. Введение класса Timer может дополнить последовательную программу, потому что класс TimerTask выполняется в потоке, управляемом классом Timer , а не приложением. Если TimerTask получает доступ к данным, к которым уже получен доступ из других потоков приложения, тогда не только TimerTask должен делать это в потокобезопасной манере, но также должны поступать и любые другие классы, получающие доступ к данным. Часто, простейшим способ достигнуть этого, является гарантия того, что объекты, к которым получает доступ класс TimerTask , сами по себе потокобезопасны, таким образом, инкапсулируя потокобезопасность внутри совместно-используемых объектов. Сервлеты и JavaServer Pages (JSPs). Фрэймворк сервлетов предназначается для обработки задач всей инфраструктуры развёртывания веб приложения и диспетчеризации запросов от удалённых HTTP клиентов. Запрос, поступающий на сервер, отправляется, возможно, через цепочку фильтров, соответствующей странице JSP или сервлету. Каждый сервлет представляет собой компонент логики приложения, и для крупных вебсайтов является нормой, что множество клиентов могут обращаться к одному и тому же сервлету одновременно. Спецификация сервлета требует, чтобы сервлет был готов к тому, что может быть вызван одновременно во множестве потоков. Другими словами, сервлет должен быть покобезопасен. Даже если вы сможете гарантировать, что сервлет будет вызываться одновременно только из одного потока, вам всё равно придётся обратить внимание на безопасность потоков при создании веб приложения. Сервлеты часто получают доступ к информации о состоянии, разделяемой (shared) с другими сервлетами, такой, как объекты с областью видимостью уровня приложения 10 (те, что хранятся 10 Область видимости application-scoped в ServletContext ), или объекты с областью видимостью уровня сессии 11 (те, что хранятся для каждого клиента в HttpSession ). Когда сервлет получает доступ к объектам, разделяемым между сервлетами и запросами, он обязан должным образом координировать доступ, чтобы множество запросов могло обращаться к ним одновременно из разных потоков. Сервлеты и JSP, так же как фильтры сервлетов и объекты, хранимые в контейнерах с областью видимости, подобные ServletContext и HttpSession , просто должны быть потокобезопасны. Удалённый вызов методов (RMI) RMI позволяет вам вызывать методы объектов, запущенных в других JVM. Когда вы вызываете удалённый метод с RMI, аргументы метода упаковываются (маршализуются) в поток байтов и отправляются по сети к удалённой (remote) JVM, где они распаковываются (демаршализуются) и передаются удалённому (remote) методу. Когда код RMI вызывает удалённый объект, в каком потоке происходит этот вызов? Вы не знаете, но определённо не в том потоке, который создали вы – ваш объект будет вызван в потоке, управляемом RMI. Как много потоков может создать RMI? Может ли быть вызван некоторый удалённый метод в некотором удалённом объекте одновременно во множестве потоков RMI? 12 Удаленный объект должен иметь защиту от двух угроз безопасности потоку: должным образом, координированный доступ к состоянию, которое может совместно использоваться с другими объектами, и должным образом скоординированный доступ к состоянию самого удаленного объекта (так как тот же объект может быть вызван в нескольких потоках одновременно). Подобно сервлетам, объекты RMI должны быть готовы к множеству одновременных вызовов и должны обеспечивать собственную потокобезопасность. Swing и AWT. Приложения GUI по своей природе асинхронны. Пользователи могут выбрать элемент меню или нажать на кнопку в любое время, и они ожидают, что приложение будет реагировать быстро, даже если оно находится в процессе выполнения чего-нибудь другого. Swing и AWT для решения этой проблемы создают отдельный поток, который занимается обработкой инициированных пользователем событий и обновлением графического представления, отображаемого пользователю. Компоненты Swing, такие как JTable не потокобезопасны. Вместо этого, программы на Swing достигают собственной потокобезопасности, ограничивая все обращения к компонентам GUI только потоком событий. Если приложение хочет манипулировать GUI извне потока событий, оно должно вызывать код 13 , который будет управлять GUI, для запуска в потоке событий. Когда пользователь выполняет UI 14 действие, в потоке событий вызывается обработчик событий для выполнения любой операции, запрошенной пользователем. Если обработчику требуется доступ к состоянию приложения, доступ к которому также происходит из других потоков (например, редактируемому документу), тогда обработчик событий, наряду с другим кодом, обращающимся к этому состоянию, должен выполнять его потокобезопасным способом. 11 Область видимости session-scoped 12 Ответ: да, но это не все, что понятно из Javadoc — для большей информации прочитайте спецификацию RMI. 13 Можно из кода отправить компоненту сообщение о событии. 14 UI – user interface/пользовательский интерфейс |