Saint Petersburg, 2012 Java Lecture #4 Multithreading.

Презентация:



Advertisements
Похожие презентации
Параллелизм и потоки в Java For students of university Author: Oxana Dudnik.
Advertisements

Многопоточное программирование на Java Java Advanced.
Многопоточное программирование на Java Java Advanced.
Лекция 6 Понятие операционных систем Учебные вопросы: 1. Характеристики ОС 2. Свободные и проприетарные ОС.
ПОТОКИ Начальные сведенияПОТОКИ Начальные сведения.
Основные виды ресурсов и возможности их разделения.
1 Java 14. Параллельное выполнение. 2 Терминология При переводе на русский ДВА английских термина имеют одинаковое значение – поток: stream thread Thread.
Модели транзакций Параллельное выполнение транзакций.
Лекция 3. Исключения и прерывания в встроенных системах.
OOП Инна Исаева. Подпрограмма – это большая программа, разделённая на меньшие части. В программе одна из подпрограмм является главной. Её задача состоит.
Лекция 4. Режимы работы микропроцессора. Взаимодействие микропроцессора с остальными устройствами Взаимодействие МП с остальными устройствами МПС происходит.
Прерывания Определение прерывания Прерывания представляют собой механизм, позволяющий координировать параллельное функционирование отдельных устройств.
POSIX Threads. Общая модель Программа Общая память Поток 1 CPU Поток 2 Поток N Потоки – наборы инструкций, исполняющиеся на CPU. Все потоки одной программы.
Объектно-ориентированное программирование Карпов В.Э. Смолток. Лекция 4. Байт-код.
Выполнили: Мартышкин А. И. Кутузов В. В., Трояшкин П. В., Руководитель проекта – Мартышкин А. И., аспирант, ассистент кафедры ВМиС ПГТА.
6. Средства синхронизации и взаимодействия процессов 6.1. Проблема синхронизации Процессам Процессам часто нужно взаимодействовать друг с другом, например,
Операционные системы Процессы и потоки Скрипов Сергей Александрович 2009.
ТЕОРИЯ И ПРАКТИКА МНОГОПОТОЧНОГО ПРОГРАММИРОВАНИЯ Тема 6 Проблемы и специфика параллельного программирования Д. ф.- м. н., профессор А. Г. Тормасов Базовая.
Учебный курс Операционные среды, системы и оболочки Лекция 5 Лекции читает доктор технических наук, профессор Назаров Станислав Викторович.
OpenGL и Direct3D сравнение стандартов Выполнил: Пенкин А. Группа И-204.
Транксрипт:

Saint Petersburg, 2012 Java Lecture #4 Multithreading

Agenda Параллельное выполнение кода Thread-safety Multithreading в языке Java JVM и потоки в рантайме Типовые грабли при написании concurrent-кода Java memory model Типовые архитектурные решения для concurrent- приложений Дополнительная литература

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

Процесс Реализацией понятия задачи в многозадачных ОС является процесс Процесс – фактическая реализация программного кода Часто в это понятие включают предоставляемые ресурсы: адресное пространство, глобальные переменные, регистры, стек, дескрипторы Эти ресурсы предоставляются процессу в выделенное пользование Процессы могут взаимодействовать опосредованно, через pipes, сокеты, сигналы, сообщения, CORBA, разделяемые файлы или память Современные ОС, как правило, ограничивают возможности процессов по управлению другими процессами

Thread (Поток) Правильный перевод Thread – «нить», но термин «поток» является общепринятым – спасибо Microsoft Press. Не путать с потоками ввода/вывода (Stream)! Потоки осуществляют распараллеливание задачи уже в рамках процесса В отличие от процессов, они используют разделяемые ресурсы и данные, могут гибко взаимодействовать между собой В монопольном владении потока находятся только его стек и содержимое регистров процессора, все остальные ресурсы потенциально разделяемы Это огромное преимущество и источник большого количества ошибок одновременно Выделяют два основных подхода к реализации потоков: Kernel mode threads и надстройки над ними User mode threads, они же Green threads

Преимущества многотопочной архитектуры перед многопроцессной Упрощение программы за счет использования общего адресного пространства Общение потоков между собой гораздо легче организовать, чем общение процессов При этом оно происходит гораздо эффективнее и быстрее Меньшие относительно процесса временные затраты на создание потока и управление им В случае, если это Green Threads, то есть программная эмуляция потоков, ОС вообще может не участвовать в жизненном цикле потока Повышение производительности процесса за счет распараллеливания процессорных вычислений и операций ввода/вывода

Взаимодействие потоков Потоки разделяют ресурсы, поэтому при доступе к ним необходима синхронизация Простой пример: пусть есть банковский счет ($100) и запросы к нему обслуживаются многопоточной системой В таком случае возможно совершить два одновременных запроса на списание $75 и оба будут удовлетворены – в обоих случаях сумма списания меньше остатка на счете Более того, в результаты работы потока могут не сразу попадать в оперативную память, находясь в кэше, например в кэше процессора В этом случае другие потоки не увидят изменений общих данных в памяти

Семафор Переменная целого типа, управляющая доступом к ресурсу или разделяемым данным Её фактическое значение – количество потоков, использующих ресурс в данный момент Если очередной поток пытается получить ресурс, но переменная уже достигла максимума, то поток вынужден ждать Если максимум – N, то семафор называется N-местным Наиболее употребительны одноместные семафоры, называемые также мьютексами (mutex)

Синхронизация Под синхронизацией понимают управление порядком взаимного исполнения потоков Наиболее распространенный способ синхронизации – создание критической секции При этом началом критической секции служит операция захвата мьютекса Концом критической секции будет операция освобождения мьютекса Таким образом, в любой момент времени код критической секции исполняется не более чем одним потоком Остальные потоки вынуждены ждать на операции захвата мьютекса до тех пор, пока он не освободится

Производительность Казалось бы, многопоточный код выигрывает в производительности Тем не менее всегда есть накладные расходы CPU на переключение контекста потока и памяти на хранение копий регистров и стека Синхронизация также требует накладных расходов: Как правило компилятор не может применять многие из оптимизаций к содержимому критических секций Синхронизация требует сброса кэшей процессора и обработчика памяти Если реализация многопоточности в языке не слишком эффективна, то однопоточное решение может в итоге оказаться производительнее Чтобы избежать переключения контекста иногда применяется busy waiting (aka spinlock) – работа потока вхолостую пока некоторое условие не будет достигнуто. В современных языках высокого уровня самостоятельная реализация этой техники редко оправдывает себя и считается антипаттерном.

Agenda Параллельное выполнение кода Thread-safety Multithreading в языке Java JVM и потоки в рантайме Типовые грабли при написании concurrent-кода Java memory model Типовые архитектурные решения для concurrent- приложений Дополнительная литература

Thread-safety Код называется thread-safe, если он обеспечивает соблюдение своего контракта в многопоточном окружении Распространенной техникой достижения thread-safety является написание реентерабельного кода Код, не являющийся thread-safe, требует синхронизации при работе в многопоточном окружении Для языка Java справедливо thread safety = atomicity + visibility Информацию о том, является ли класс из JDK thread-safe часто можно найти в его javadoc

Атомарность операций Операция является атомарной, если в процессе её выполнения не может произойти передача управления другому потоку Атомарность операций на разделяемых данных – необходимое условие thread-safety Атомарность той или иной операции может быть весьма неочевидна Рассмотрим для примера следующий код:

Атомарность операций Этот код не является атомарным, поскольку включает в себя операции чтения, сложения с единицей и записи Если у нас есть два потока A и B, то порядок выполнения вполне может выглядеть так Неатомарные операции на разделяемых данных не являются thread-safe Синхронизацию можно рассматривать как средство достижения атомарности произвольной операции

Под visibility понимается свойство многопоточной архитектуры, при котором изменения, сделанные одним потоком, видны другому потоку Существует масса препятствий для этого: кэши процессора нескольких уровней и кэш обработчика памяти Если для достижения visibility придется эти кэши инвалидировать, то вместо прироста производительности на многопоточной архитектуре мы получим обратный эффект Visibility

Agenda Параллельное выполнение кода Thread-safety Multithreading в языке Java JVM и потоки в рантайме Типовые грабли при написании concurrent-кода Java memory model Типовые архитектурные решения для concurrent- приложений Дополнительная литература

Класс Thread Единственным способом создать поток без обращения к native-ресурсам является создание экземпляра класса Thread При этом потоку необходимо передать код для исполнения либо унаследовавшись от Thread и переопределив метод run() либо передав в конструктор реализацию интерфейса Runnable JVM гарантирует, что поток будет выполняться, пока не выполнено одно из следующих условий: Поток выполнил все необходимые действия и его метод run() вернул управление Поток выбросил необработанное исключение Был вызван метод Runtime.exit() или System.exit() Поток является daemon, причем в приложении не осталось активных не-daemon потоков Произошел крах самой JVM

Интерфейс Runnable Интерфейс Runnable состоит из одного метода run() Основная его задача – содержать в себе код, который будет выполнять Thread Thread также реализует Runnable Runnable часто используется для передачи исполняемого кода в качестве параметра и реализации замыканий (Closure)

Thread: API reference Статические методы: сurrentThread() - возвращает ссылку на текущий поток yield() – используется для того, чтобы уступить возможность выполняться другим потокам. setDefaultUncaughtExceptionHandler() – назначает обработчик неперехваченных исключений для потока, очень интенсивно используется в фреймворках sleep() - приостанавливает выполнение потока на указанное количество миллисекунд interrupted() – проверяет, был ли прерван текущий поток, причем флаг прерывания будет сброшен после проверки Нестатические методы: getId() и getName() – возвращают идентификационную информацию о потоке interrupt() – отправляет потоку сигнал о прерывании, по факту выставляет флаг. Целевой поток должен сам проверять флаг и корректировать поведение. join() – текущий поток блокируется до окончания выполнения целевого потока start() – запускает выполнение потока run() – запускать этот метод не хочешь ты, юный падаван

Прерывание работы потока Несмотря на то, что для Thread определены методы stop(), destroy(), suspend() и resume(), все Самая первая парадигма управления потоками в Java отводила этим методам значительную роль, но их так и не удалось реализовать безопасным образом – все эти методы имеют шанс вызвать deadlock Сегодня управление другими потоками построено по другой схеме: после старта родительский поток не может остановить дочерний в принудительном порядке Все, что можно сделать для прерывания потока – это вызвать метод interrupt() у этого потока. Этот вызов установит флаг прерывания и целевой поток должен сам проверять и реагировать на этот флаг Проще говоря, мы можем только рекомендовать потоку завершиться и не более того Вывод: В Java-приложении поток должен сам проверять целесообразность дальнейшей работы и не полагаться на принудительные прерывания со стороны других потоков

Ключевое слово Synchronized Synchronized позволяет организовывать критические секции средствами языка Java Применяется в двух основных формах: В виде synchronized-блока В виде synchronized-метода Мьютексами для этих блоков служат так называемые мониторы Monitor – структура, ассоциированная с Java-объектом, которая может выполнять роль мьютекса Для synchronized-блока мьютексом служит монитор объекта-аргумента Для статического synchronized-метода мьютекcом будет монитор объекта типа Class для класса, содержащего статический метод Synchronized гарантирует, что мьютекс будет отпущен в любом случае, даже если будет брошено исключение

Wait(), notify(), notifyAll() Эти методы класса Object позволяют работать с ассоциированным монитором Вызов wait() отпускает мьютекс и переводит поток в wait set монитора Вызов notify() пробуждает случайно выбранный поток из wait set монитора. Как только мьютекc будет освобожден пробужденный поток сможет его захватить Вызов notifyAll() подобным образом пробуждает все потоки, находящиеся в wait set мьютекса Пример справа – реализация блокирующего буфера

Daemon – поток, выполняющий служебные функции в фононовом режиме Основное его свойство – наличие выполняющегося демона не является препятствием для завершения работы JVM JVM завершает свою работу когда завершается последний не-daemon поток Очевидно все служебные потоки JVM являются daemon Свойство daemon устанавливается вызовом метода setDaemon(boolean value) на любом потоке В качестве daemons чаще всего выступают разного рода recovery-потоки Daemons

Таймеры java.util.Timer позволяет выполнять задачи по расписанию Он позволяет запланировать однократное или периодическое выполнение Он принимает TimerTask, простую реализацию Runnable, предоставляющую несколько дополнительных методов cancel() - снимает задачу из расписания scheduledExecutionTime() - возвращает время, на которое запланировано выполнение задачи Таймеры ждут и выполняют код в отдельном потоке, по одному на таймер Создавать сотни тысяч таймеров – плохая идея Тяжелые задачи нельзя выполнять непосредственно в потоке таймера – они могу задержать выполнение следующего периодического вызова Потоки таймеров по умолчанию не является daemon Это означает, что забытый таймер может поддерживать работу приложения, хотя все остальные бизнес-потоки уже завершили свое выполнение Существует также javax.swing.Timer, не стоит его использовать если вы не работаетe со Swing

Таймеры - Пример Неправильный вариант Правильный вариант

Работаем с процессами JVM, как правило, работает и исполняет байткод в рамках одного процесса Тем не менее, существует API для работы с другими процессами Самый простой способ – Runtime.getRuntime.exec() – исполняет переданную строку как консольную команду операционной системы Очевидно такой способ работает только для конкретной платформы Метод exec() возвращает реализацию Process, которая содержит методы для работы с созданным процессом Другим способом является использование класса ProcessBuilder При этом будет унаследовано все окружение родительского процесса: рабочая директория, переменные окружения Их можно изменить отдельными методами перед тем, как запускать процесс методом start() Стандартные потоки вывода и ошибок надо либо вычитывать, либо перенаправлять, иначе процесс может быть заблокирован

Agenda Параллельное выполнение кода Thread-safety Multithreading в языке Java JVM и потоки в рантайме Типовые грабли при написании concurrent-кода Java memory model Типовые архитектурные решения для concurrent- приложений Дополнительная литература

Потоки JVM Даже если вы не создаете потоков в явном виде, один поток JVM уже создала при запуске вашего приложения – main thread В нем начинается выполнение метода main() JVM также запускает потоки для исполнения служебных задач: финализации, сборки мусора и других Все современные фреймворки (Struts, Spring, EJB, RMI, etc) создают потоки для собственных нужд и для выполнения кода При этом они осуществляют вызов кода разработчика из своих потоков Помимо прочего, это означает, что любой код для такого фреймворка должен быть Thread-safe Свой поток будет создан и для таймеров

Потоки JVM

Работа с фреймворками В качестве иллюстрации можно рассмотреть механизм работы контейнера сервлетов Хотя разработчик не создает потоки в явном виде, его код (сервлеты) работает в многопоточном окружении

Жизненный цикл потока Thread может находиться в 6 различных состояниях Актуальное состояние всегда можно узнать при помощи метода Thread.getState()

Monitor Схема ниже иллюстрирует взаимодействие Java-потоков с монитором В любой момент времени только один поток может быть владельцем монитора Wait set содержит потоки, которые, находясь в критической секции, добровольно отпустили мьютекс и приостановили свое выполнение вызовом метода wait()

Планировщик потоков Планировщик потоков решает, какой поток в какой момент времени будет выполняться Задача планировщика – обеспечить параллельное выполнение M потоков на N процессорах (ядрах) Планировщик учитывает приоритеты, но не следует им строго Это связано с оптимизациями, которые планировщик делает для удешевления переключения контекста потоков В общем случае поведение планировщика зависит от конкретной ОС и аппаратной платформы Поток может явно вернуть управление планировщику при помощи вызова статического метода Thread.yield()

Планировщик потоков

Приоритеты потоков Методы Thread.setPriority() и Thread.getPriority() позволяют установить приоритет для потока Чем выше приоритет, тем больше времени планировщик будет отдавать данному потоку Задавать значения можно в диапазоне 1-10 вне зависимости от того, какой диапазон приоритетов поддерживает ОС По умолчанию потоку присваивается приоритет породившего его потока Строить логику на приоритетах потоков нельзя – скорее всего планировщик будет запускать более приоритетные потоки чаще, но он не обязан этого делать Приоритеты могут применяться для тонкого тюнинга производительности многопоточной системы, но только на основании экспериментальных данных

Приоритеты потоков

Атомарность операций Простые арифметические операции на большинстве примитивов атомарны Операции с double и long неатомарны, так как требуют использования двух инструкций некоторых машинах Чтобы добиться атомарности определенной операции в общем случае необходимо поместить её в критическую секцию Поэтому атомарность – достаточно сильное требование Для обеспечения атомарности операций в Java существует ряд оберток над примитивами: AtomicInt, AtomicLong и другие. Любые операции на этих объектах будут атомарны Чтобы сделать атомарной операцию на произвольном объекте необходимо синхронизировать доступ к разделяемому состоянию, затрагиваемому этой операцией, простой синхронизации метода, реализующего эту операцию может быть недостаточно

Contended/uncontended блокировки Блокировка называется contended, если за на нее действительно претендует несколько потоков одновременно JIT-компилятор ограничен в оптимизациях подобных блокировок Uncontended-блокировки не испытывают попыток одновременного доступа к ресурсам Поэтому к ним могут применяться более агрессивные оптимизации вплоть до удаления блокировки вообще Как правило классификацию блокировок JVM проводит уже в рантайме на основании собранной статистики

JIT-оптимизации для synchronized Эти оптимизации применяются в версиях 1.6 (Mustang) и старше JIT-компилятор может совсем убрать блокировку, если escape-анализ показывает её локальность В этом случае ресурс даже теоретически не может быть доступен из нескольких потоков Кандидат на пропуск блокировки: Как и многие другие, эта оптимизация может быть отменена на основании новых данных, собранных JVM

JIT-оптимизации для synchronized JIT-компилятор также может укрупнить блокировку, избегая по возможности переключения контекста Кандидат на укрупнение блокировки: Если статистика использования монитора показывает, что он захватывается на непродолжительное время, то блокировка может стать адаптивной В таком случае ожидающий поток не паркуется сразу на занятом мьютексе, а некоторое время крутит спинлок, проверяя доступность монитора CAS-операцией

CAS и неблокирующие алгоритмы Неблокирующим называется алгоритм, обеспечивающий thread-safety без ожидания на мониторе (wait-free) Как обеспечить atomicity и visibility без memory barriera? Compare-and-set (compare-and-swap, CAS) – инструкция, поддерживаемая на уровне процессора (lock:cmpxchg) Она позволяет сравнить значение с содержимым памяти и при совпадении выполнить запись Эта инструкция позволяет применять оптимистичные блокировки без переключения контекста потока при занятом ресурсе Принцип работы CAS в псевдокоде: CAS на многопроцессорных машинах будет дороже из-за аппаратной реализации атомарности операции

CAS и неблокирующие алгоритмы В качестве примера рассмотрим генератор последовательных чисел Он является thread-safe При этом ни один поток не будет заблокирован на мьютексе операционной системы

Biased locking Большая часть блокировок являются uncontended, однако escape-анализа недостаточно для их определения В качестве оптимизации JVM пытается осуществить привязку (biasing) монитора к конкретному потоку, чтобы избежать лишних CAS Стоимость такой привязки эквивалентна одной CAS-операции При последующих захватах монитора этим же потоком на всю синхронизацию требуется одна (sic!) операция сравнения, проверяющая, действительно ли монитор все еще проассоциирован с id этого потока В случае uncontended-блокировки мы получаем практически бесплатную синхронизацию Если же contention все же имеет место, то происходит отзыв biased-блокировки Отзыв biased-блокировки весьма дорог Далее JVM использует для этой блокировки более консервативные механизмы Адаптивные спинлоки Мьютексы операционной системы

Agenda Параллельное выполнение кода Thread-safety Multithreading в языке Java JVM и потоки в рантайме Типовые грабли при написании concurrent-кода Java memory model Типовые архитектурные решения для concurrent- приложений Дополнительная литература

Race condition Этим термином обозначают неустойчивый код: в зависимости от момента передачи управления другому потоку результат выполнения может различаться В языке Java точный порядок доступа к ресурсу очень часто неспецифицирован; Если код в своей работе полагается на порядок доступа к защищенному ресурсу, то это часто приводит к race condition Основной симптом – разные результаты при запуске в казалось бы одинаковых условиях Название ошибка получила от похожей ошибки проектирования электронных схем

Race condition

Starvation Под этим понятием подразумевается ситуация, когда потоку не выделяется процессорного времени или выделяется слишком мало для нормальной работы В Java причинами такой ситуации могут быть Наличие большого количества высокоприоритетных потоков, не дающих выполняться низкоприоритетным Существование потоков с эксклюзивным доступом в критические секции, так что другие потоки вынуждены ожидать на мьютексе очень долго Удержание потоком мьютекса на очень долгое время, так что другие потоки могут ждать на нем практически вечно Существование большого количества потоков, порядка десятков тысяч и неправильно выбранный сборщик мусора. Тут в состоянии starvation будут все рабочие потоки, а большую часть CPU будет потреблять GC Внешним симптомом starvation является относительно низкая производительность одного или нескольких потоков Решением этой проблемы может быть использование Fair Locks

Deadlock Deadlock – это ситуация взаимной блокировки потоков, при которой они не могут продолжать выполнение, ожидая друг друга Чаще всего это происходит когда нескольким потокам для выполнения операций требуется несколько разных синхронизированных ресурсов Синхронизированные ресурсы подразумевают монопольный доступ При этом каждый поток уже захватил часть ресурсов, но не может получить оставшиеся и стоит в ожидании Хотя JVM в какой-то мере способна распознавать deadlock, она ничего не предпринимает для разрешения этой ситуации

Deadlock - Пример Thread 1 вызывает parent.addChild(child); Thread 1 получает мьютекс на parent Thread 2 вызывает child.setParent(parent); Thread 2 получает мьютекс на child Thread 1 пытается выполнить child.setParentOnly(parent ) и не может – child залочен потоком Thread 2 Thread 2 пытается выполнить parent.addChildOnly() и тоже не может – parent залочен потоком Thread1

Livelock Livelock – состояние подвижной блокировки, при котором потоки активно работают, но прогресса достичь не могут из-за взаимной блокировки ресурсов. Livelock – гораздо более редкая и труднодиагностируемая ситуация, чем Deadlock Livelock –На диаграмме справа представлена работа «умных» потоков – если они не могут захватить все нужные ресурсы, то они отпускают остальные Как видно, они вполне способны заблокировать друг друга динамически, занимая и освобождая ресурсы постоянно

Agenda Параллельное выполнение кода Thread-safety Multithreading в языке Java JVM и потоки в рантайме Типовые грабли при написании concurrent-кода Java memory model Типовые архитектурные решения для concurrent- приложений Дополнительная литература

Java memory model По сути, Java – первый из популярных языков программирования, для которого была разработана модель памяти Результат работы многопоточного приложения зависит от atomicity, visibility и reordering отдельных операций непосредственным образом Без модели памяти уровня языка эти свойства зависят от конкретных компилятора и процессора Модель памяти определяет набор правил, определяющий указанные выше параметры вне зависимости от операционной системы, типа и количества процессоров В языках типа C++ ваш код напрямую зависит от свойств целевой программно-аппаратной платформы (до С++11) Преимущества Java Memory Model активно используются языками на платформе JVM, такими как Scala или Clojure

Happens-before Основная абстракция Java Memory Model Happens-before устанавливает связь двух событий в разных потоках Если обозначить эти события как X и Y, то все события потока A до события X видимы (visible) для всех событий потока B после Y Happens-before гарантирует visibility, но не гарантирует атомарности операции

Happens-before В рамках одного потока любая операция happens-before любой операции следующей за ней в исходном коде Освобождение Lock (unlock) happens-before захвата того же Lock (lock) Выход из synhronized блока/метода happens-before входа в synhronized блок/метод на том же мониторе Запись volatile поля happens-before чтения того же самого volatile поля Завершение метода run экземпляра класса Thread happens-before выхода из метода join() или возвращения false методом isAlive() экземпляром того же потока Вызов метода start() экземпляра класса Thread happens-before начала метода run() экземпляра того же потока Завершение конструктора happens-before начала метода finalize() этого класса Вызов метода interrupt( ) на потоке happens-before момента, когда поток обнаружил, что данный метод был вызван либо путем выбрасывания исключения InterruptedException, либо с помощью методов isInterrupted() или interrupted()

Reordering Абстракция happens-before очень важна в контексте reordering – переупорядочивания bytecode-инструкций во время выполнения Reordering производится в целях оптимизации как на уровне JIT- компилятора, так и на уровне процессора При этом два события, связанные отношением happens-before не могут быть переупорядочены Reordering никогда не переставляет местами инструкции одного потока Это означает, что в однопоточном приложении reordering никогда не проявляется Кроме того, Java Memory Model изменила семантику ключевого слова volatile – теперь переупорядочивание volatile-инструкций с обычными невозможно

Reordering Reordering может быть не только программным, но и аппаратным Большинство процессоров переупорядочивают операции с целью оптимизации Разные архитектуры налагают разные ограничения на возможность переупорядочивать инструкции Любая программная memory model должна учитывать их для сохранения консистентности разделяемых данных данных Reordering type AlphaARMv7PA-RISCPOWER SPARC RMO SPARC PSO SPARC TSO x86AMD64IA64zSeries Loads reordered after LoadsYYYYYY Loads reordered after StoresYYYYYY Stores reordered after StoresYYYYYYY Stores reordered after LoadsYYYYYYYYYYY Atomic reordered with LoadsYYYYY Atomic reordered with StoresYYYYYY Dependent Loads reorderedY Incoherent Instruction cache pipelineYYYYYYYYY

Семантика volatile Семантика ключевого слова volatile была существенно изменена в JSR-133 Во всех версиях Java volatile обеспечивает visibility, то есть операции на volatile-переменных идут мимо кэшей сразу в память Начиная с Java 1.5 volatile-переменные также устанавливают отношение happens-before между записью и чтением такой переменной: запись happens-before чтения Reordering volatile-инструкций с обычными инструкциями также запрещен С усилением гарантий для volatile-переменных возросли также и накладные расходы на их использование Модификатор volatile часто воспринимается как облегченная форма синхронизации доступа, но это не совсем так Если вы пытаетесь заменить синхронизацию на volatile из соображений производительности, подумайте еще раз – непросто учесть сторонние эффекты такой замены

Agenda Параллельное выполнение кода Thread-safety Multithreading в языке Java JVM и потоки в рантайме Типовые грабли при написании concurrent-кода Java memory model Типовые архитектурные решения для concurrent- приложений Дополнительная литература

Immutable-объекты Это объекты, которые нельзя изменять после создания Такие объекты не требуют синхронизации Многие стандартные классы языка Java – immutable java.lang.String Все объектные обертки над простыми типами java.math.BigInteger и java.math.BigDecimal Помимо этого immutable-объекты обладают и другими преимуществами Они не могут находится в inconsistent-состоянии Реализация hashcode() может кэшировать значение " Classes should be immutable unless there's a very good reason to make them mutable....If a class cannot be made immutable, limit its mutability as much as possible." Effective Java, Joshua Bloch

Thread pooling Создание и удаление потока может быть достаточно дорогой операцией Необходима поддержка потоков на уровне ОС Потоку требуется выделить ряд ресурсов, причем не все из них поток может эффективно использовать. Например, Windows не может выделить меньше 64К под стек потока. Альтернатива – использование пула потоков Пул управляет несколькими рабочими потоками, выполняя задачи из очереди по мере возможности

Double-checked locking Это паттерн, применяемый для реализации шаблона Singleton в многопоточной среде Он позволяет избежать затрат на синхронизацию, если объект уже создан Типовая реализация выглядит так:

Double-checked locking … и она не работает Из за переупорядочивания операций второй поток может получить ссылку на не до конца сконструированный объект Начиная с Java 1.5 модификатор volatile на переменной исправляет ситуацию Тем не менее, синхронизация сейчас гораздо дешевле, чем она была во времена изобретения этого паттерна Вывод: с использованием возможностей JSR-133 этот шаблон можно заставить работать правильно, но зачем?

Double-checked locking: Альтернативы Поскольку синхронизация на современных JVM достаточно быстрая можно просто синхронизировать метод getInstance() За счет memory barrierа решаются все проблемы с переупорядочиванием инструкций 63 Другой вариант решения – initialization-on-demand holder Основан на том, что JVM загружает классы только в момент первого использования

GUI Toolkits Делать GUI без поддержки многопоточности нельзя – любая «тяжелая» задача заблокирует перерисовку компонент и обработку GUI-событий Поэтому общим правилом является отделение тяжелых задач бизнес-логики от перерисовки и обработки событий При таком подходе GUI не «зависает» даже если выполнение рабочего потока задерживается Существуют разные модели многопоточности для GUI Toolkitов: AWT использует thread-safe компоненты Swing формально однопоточен, но содержит средства для выполнения длительных задач в рабочих потоках. Его компоненты не являются thread-safe SWT в этом контексте во многом похож на Swing, но относится к ошибкам многопоточного кода гораздо строже.

Server threading models Существует несколько моделей распределения нагрузки по потокам внутри сервера: Single thread – single client При этом единственный поток обрабатывает клиентский запрос. Пока он занят остальные запросы получают отказ в обслуживании. Single thread, multiple clients Этот подход требует наличия одного потока-исполнителя и очереди клиентских запросов Неплохо справляется с небольшим количеством запросов, но критикуется за отсутствие масштабируемости Thread per client Подразумевает выделение по одному потоку на обработку каждого клиентского запроса Применительно к языку Java, с ростом количества активных потоков выше определенного порога падает эффективность GC и возрастают накладные раcходы на context switch Пул потоков заданной величины и очередь ожидания для клиентов могут существенно улучшить данный подход

Library Brian Goetz. Java concurrency in practice Java Language Specification, глава 17 Maurice Herlihy, Nir Shavit. The art of multiprocessor programming Статьи Brianа Goetzа на (например 66