В то время как в предшествующих главах речь
идет только о поведении Ява-кода, где в каждый
момент времени выполняется единственный
оператор или выражение, то есть единственный поток,
каждая виртуальная машина языка Ява может
поддерживать несколько потоков выполнения
одновременно. Эти потоки независимо друг от
друга выполняют Ява-код, который использует
значения и объекты языка Ява, расположенные в
разделяемой основной памяти. Потоки могут быть
поддержаны при наличии нескольких процессоров,
режимом разделения времени одного процессора,
или режимом разделения времени нескольких
процессоров.
Язык Ява поддерживает кодирование программ,
которые, хотя и действуют одновременно, но все еще показывают
детерминированное поведение, обеспечивая
механизмы для синхронизирования
параллельной деятельности потоков. Чтобы
синхронизировать потоки, язык Ява использует мониторы,
которые являются механизмами высокого уровня
для разрешения только одного потока в единицу
времени, чтобы выполнить часть кода защищенного
монитором. Поведение мониторов объясняется в
терминах замков; имеется замок связанный с
каждым объектом.
Оператор synchronized (§14.17)
выполняет два специальных действия, которые
имеют место только в многопоточных операциях: (1)
после вычисления ссылки на объект, но перед
выполнением его тела, блокирует замок,
связанный с объектом, и (2) после выполнения тела
заканчивает, или нормально, или преждевременно,
он разблокирует тот же самый замок. Для
удобства, метод может быть объявлен как synchronized; такой метод ведет себя как тело, которое содержится в
операторе synchronized.
Методы wait (§ 20.1.6, §
20.1.7, § 20.1.8), notify (§
20.1.9), и notifyAll (§ 20.1.10) класса Object поддерживают эффективную
передачу управления от одного потока к другому.
Более проще чем просто "прядение"
(неоднократно блокируя и разблокируя объект,
чтобы видеть, изменилось ли некоторое внутреннее
состояние), которое потребляет вычислительную
работу, поток может
приостановить себя используя wait
до такого времени как другой поток “проснется”
используя notify. Это особенно
свойственно для тех ситуаций, где потоки имеют связь создателя-потребителя
(активно сотрудничающего для общей цели) быстрее,
чем отношение взаимного исключения (пробующий
избегать конфликтов при разделении общего
ресурса).
Поскольку поток выполняет код, он выполняет
последовательность действий. Поток может
использовать значение переменной или присваивать
ей новое значение. (Другие действия включают
арифметические операции, проверки условия, и вызов метода, но они не включают в
себя непосредственно переменные.) Если два или
больше параллельных потока действуют на
разделяемой переменной, существует возможность,
что действия на переменной произведут зависящие от времени результаты.
Эта зависимость от времени свойственна
параллельному программированию, создающему одно
из немногих мест в языке Ява, где результат
выполнения программы не определен с помощью этой
спецификации.
Каждый поток имеет рабочую память, в которой он
может хранить копии значений переменных из
основной памяти, которая разделяется между всеми
потоками. Обращаясь к разделяемой переменной,
поток обычно сначала получает замок и подавляет
его рабочую память. Это гарантии того, что
разделяемые значения, которые будут загружены
после этого из разделяемой основной памяти
потокам рабочей памяти. Когда
поток разблокирует замок, то значения
содержащиеся в рабочей памяти, будут снова
записаны в основную память.
Эта глава объясняет взаимодействие потоков с
основной памятью, и таким образом друг с другом, в
терминах определенных действий низкого уровня.
Существуют правила относительно порядка в
которых эти действия могут происходить. Эти
правила налагают ограничения на любую
реализацию в языке Ява, и программист может
полагаться на правила, чтобы предсказать
возможное поведение параллельного
программирования языка Ява. Правила
, однако, дают реализации некоторые свободы;
намерение состоит в том, чтобы разрешить
некоторые стандартные аппаратные средства ЭВМ и
методы программного обеспечения, которые могут
улучшать скорость и эффективность параллельного
кода.
Некоторые важные краткие следствия из правил:
- Рациональное использование синхронизации
состоит в разрешении надежной передачи значений
или множеств значений от одного
потока другому через разделенные переменные.
- Когда поток использует значение переменной, то
это значение получает - фактическое значение
сохраненное тем же потоком в переменной или
некоторым другим потоком. Это верно даже если
программа не содержит кода для надлежащей
синхронизации. Например, если два потока хранят
ссылки на различные объекты в одном и том же
самом значении ссылки, переменная будет
впоследствии содержать ссылку на один или другой
объект, который не ссылается на некоторый другой
объект или испорченное значение ссылки. (
Существует специальное исключение для
long и double; см. § 17.4.)
- В отсутствии явной синхронизации, реализация языка Ява свободно корректируется
основной памятью в порядке который может быть неожиданным. Поэтому
программист который предпочитает избегать
сюрпризов использует явную синхронизацию.
Переменная - это некоторая область памяти в
пределах Ява-программы, которая может храниться
в ней. Это относится не только к переменным
класса и переменным экземпляра, но также к
компонентам массива. Переменные сохраняются в основной
памяти, которая разделяется всеми потоками.
Так как для одного потока невозможен доступ к
параметрам или локальным переменным другого
потока, это не имеет значение, так или иначе их
содержат параметры и локальные переменные
расположенные в разделяемой основной памяти или
в рабочей памяти потока.
Каждый поток имеет рабочую память, в
которой хранит собственную рабочую копию
переменных, которые должны использоваться или
присваиваться. Так как поток
выполняет Ява-программу, то это работает на этих
рабочих копиях. Основная память содержит главную
копию каждой переменной. Существуют правила
относительно того, когда разрешается или
требуется поток, чтобы передать содержание его
рабочей копии переменной в главную копию или
наоборот.
Основная память также содержит замки;
имеется один замок, связанный с каждым объектом.
Потоки могут конкурировать, чтобы завладеть
замком
Цель главы - глаголы: использовать,
присваивать, загружать, хранить, блокировать
и разблокировать - это список тех действий,
которые может выполнять поток. Глаголы - читать,
записать, блокировать и разблокировать -
это список тех действий, которые может выполнять
подсистема основной памяти. Каждое из этих
действий атомарно (неделимо).
Действие использовать или присваивать -
сильносвязанное взаимодействие между основным
выполнением потока и рабочей памятью потока.
Действие блокировать и разблокировать -
сильносвязанное взаимодействие между основным
выполнением потока и основной памятью. Но
передача данных между основной памятью и рабочей
памятью потока слабосвязаны. Когда данные
копируются из основной памяти в рабочую память,
то должны происходить два действия : действие читать,
выполняемое основной памятью, следует после
соответствующего действия загружать,
выполняемого рабочей памятью. Когда данные
копируются из рабочей памяти в основную память,
должны происходить два действия: действие сохранить,
выполняемое рабочей памятью, которое
следует после соответствующего действия записать, выполняемое основной памятью. Может
существовать некоторое транзитное время между
основной памятью и рабочей памятью, и транзитное
время может быть различно для каждой транзакции;
таким образом действия, начатые потоком на
различных переменных могут рассматриваться
другим потоком как упорядочение в
другом порядке. Однако, для каждой переменной
действия в основной памяти от какого-нибудь
одного потока выполняются в том же самом порядке
как и соответствующие действия данного потока.
(Более подробней об этом смотрите ниже.)
Отдельный поток языка Ява выводит ряд действий использовать,
присваивать, блокировать и разблокировать
выполняет программу диктуясь семантикой языка
Ява. Лежащая в основе реализация языка Ява
требует дополнительно выполнения
соответствующих действий загружать, хранить,
читать и записать, при этом подчиняющихся
некоторому набору ограничений, объясненных ниже.
Если реализация языка Ява корректно следует этим
правилам и программист следует другим
определенным правилам программирования, тогда
данные могут надежно передаваться между
потоками через разделяемые переменные. Правила
разработаны чтобы быть
достаточно "напряженными", но достаточно
"свободным", и чтобы предоставить
аппаратным средствам и проектировщикам
программного обеспечения значительную свободу
для улучшения скорости и производительности
через такие механизмы как регистр, очереди и
кэши.
Имеются детальные определения каждого из
действий:
- Действие (потока) использовать передает
содержание рабочей копии переменной,
принадлежащей потоку, исполнителю потока. Это
действие выполняется всякий раз, когда поток
выполняет команду виртуальной машины, которая
использует значение переменной.
- Действие (потока) присваивать передает
значение из исполнителя потока в рабочую копию
переменной, принадлежащей потоку. Это действие
выполняется всякий раз, когда поток выполняет
команду виртуальной машины, которая
присваивается переменной.
- Действие читать (основной памяти) передает
содержание оригинала переменной рабочей памяти
потока для использования позже действием загружать.
- Действие загружать (потока) помещает значение,
переданное из главной памяти с помощью действия
читать в рабочую копию переменной потока.
- Действие сохранить (потока) передает
содержание рабочей копии потока из переменной в
основную память для использования позже
действием записать.
- Действие записать (основной памяти) помещает значение, переданное
из рабочей памяти потока действием сохранить в
копию оригинала в основной памяти.
- Действие блокировать (выполняемое потоком и
сильно синхронизированное с основной памятью)
вынуждает поток приобретать одно требование на
данный замок.
- Действие разблокировать (выполняемое
потоком и сильно синхронизированное с основной
памятью) вынуждает поток выпускать одно
требование на данный замок.
Таким образом взаимодействие потока с
переменной, состоят из последовательности
действий: использовать, присваивать, загружать,
и сохранить. Основная память исполняет
действие читать для каждого из действий загружать
и записать и для каждого действия сохранять.
Взаимодействия потока с замком состоят из
последовательности действий блокировать
и разблокировать. Все глобально видимое
поведение потока таким образом включает
действия всех действий потоков на переменных и
замках.
Правила последовательности выполнения
содержат порядок, в котором могут происходить
некоторые случаи. Существуют четыре основных
ограничения относительно действий:
- Действия, выполняемые некоторым потоком
полностью упорядочены; то есть для любых двух
действий, выполняемых потоком, одно действие
предшествуют другому.
- Действия, выполняемые основной памятью для
какой-нибудь одной переменной полностью
упорядочены; то есть для любых двух действий,
выполненных основной памятью на ту же самую
переменную, одно действие предшествует другому.
- Действия, выполняемые основной памятью для
любого замка полностью упорядочены; то
есть для любых двух действий, выполняемых
основной памятью на том же самом замке, одно
действие предшествует другому.
- Это не разрешается для действия следующего за
собой.
Последнее правило может показаться
тривиальным, но для полноты необходимо
сформулировать отдельно и подробно. Без этого,
было бы не возможно предложить набор действий c
помощью двух или большего количества потоков и
отношений предшествования среди действий,
которые удовлетворяли другим правилам но
требовали бы, чтобы действие следовало за собой.
Потоки не взаимодействуют непосредственно; они
связываются только через разделяемую основную
память. Отношения между действиями потоков и
действия основной памяти заключены в трех
случаях:
- Каждое действие блокировать или разблокировать
выполняется совместно некоторым потоком и
основной памятью.
- Каждое действие потока загружать -
уникально соединено с действием читать
основной памятью так, что действие загружать
следует за действием читать.
- Каждое действие потока сохранить -
уникально соединено с действием записать
основной памятью так, что действие записать
следует за действием сохранить.
Большинство правил в следующих далее разделах
содержат порядок, в которых имеют место
определенные действия. Правило может
устанавливать то, что одно действие должно
предшествовать или следовать за некоторым
другим действием. Заметьте, что эти отношения -
переходные: если действие A должно
предшествовать действию B, и B должно
предшествовать C, тогда А должно
предшествовать C. Программист должен помнить
что эти правила - единственные ограничения на
порядок действий; если никакое правило или
комбинация правил не подразумевают, что действие
А должно предшествовать действию B, тогда
реализация языка Ява свободно исполняет
действие B перед действием A, или
исполняет действие B одновременно с
действием A. Эта свобода может быть ключом к
хорошей производительности. Наоборот,
реализация не нуждается в
преимуществе всех данных свобод.
В правилах, которые следуют в фразе " B
должен вмешиваться между А и C "
означает что действие B должно следовать за
действием А и предшествовать действию C.
Пусть T - поток, а V - переменная.
Существуют некоторые ограничения на действия
выполняемые T относительно V:
- Использовать или присваивать T V,
разрешается только когда предписано выполнение T
программы согласно стандарта выполнения модели
Языка Ява. Например, появление V как операнда +
требует единственного действия использовать
происходящего на V; появление V как левого
операнда присваивания = требует, чтобы
происходило единственное действие присвоить.
Все действия использовать и присвоить
данного потока должны происходить в
соответствии с порядком, указанным в программе
являющейся выполняемым потоком. Если следующие
правила запрещают T исполнять требуемое
действие использовать как следующее
действие, это может быть необходимо для T,
чтобы исполнить сначала действие загружать.
- Действие сохранить T на V должно
вмешиваться между действиями присвоить T на V и последующим загрузить
T на V. (Менее формально:
потоку не разрешается терять самое последнее
действие присваивать.)
- Действие присваивать T на V должно
вмешиваться между действиями загрузить или сохранить
T на V и последующим сохранить T на V.
(Менее формально: ни по какой причине потоку не
разрешается записать данные из рабочей
памяти назад в основную память.)
- После того, как поток создан, должно выполниться
действие присваивать или загружать на эту
переменную перед выполнением действия использовать
или сохранить на эту же переменную. (Менее
формально: новый поток начинается с пустой
рабочей памятью.)
- После того, как переменная создана, каждый поток
должен выполнить действие присваивать или загружать
на эту переменную перед выполнением действия использовать
или сохранить на эту же переменную. (Менее
формально: новая переменная создается только в
основной памяти и - первоначально не
в какой рабочей памяти потока.)
При условии, что все ограничения, описанные
выше и ниже, выполняются, действия загружать
или сохранять могут быть выданы в любое время
любым потоком на любую переменную, в зависимости
от реализации.
Существуют также некоторые ограничения на
действия читать и записать, выполняемые
основной памятью:
- Для каждого действия загружать,
выполненного любым потоком T на рабочей копии
переменной V, должно существовать
соответствующее предшествие действия читать
основной памятью на оригинале V, и действие загружать
должно поместить в рабочую копию данные,
переданные соответствующим действием читать.
- Для каждого действия сохранять,
выполняемого любым потоком T на рабочей копии
переменной V, должно существовать
соответствующее предшествие действия записать
основной памятью на оригинале V, и действие записать
должно поместить в оригинал данные, переданные
действием сохранить.
- Пусть А - действие загружать или сохранять
потоком T на переменной V, и пусть Р
соответствующее действие читать или записать
основной памятью на переменной V. Также, пусть
действие B - некоторое другое действие загружать
или сохранять потоком T на этой же самой
переменной V, и пусть действие Q -
соответствующее действие читать или записать
основной памятью на переменной V. Если
А предшествует B, тогда P должен предшествовать Q.
(Менее формально: действия на оригинале любой
данной переменной от имени потока выполняемого
основной памятью в точном порядке, который
необходим потоку.)
Заметьте, что это последнее правило
применяется только к действиям потока на ту же самую переменную. Однако,
существует более строгое правило для volatile-переменных
(§ 17.7).
double и long
Если переменная типа double или long не объявлена с помощью volatile,
тогда для действий загружать, сохранить, читать,
и записать, как две переменные по 32 бита
каждая: везде, где правила требуют одно из этих
действий - выполняются два таких действия, одно
для каждой половины из 32 бит. Способ, в
котором переменная 64 бита double или long кодируется в две 32 битные величины
зависит от реализации.
Это имеет значение только потому что действия читать
или записать переменной типа double
или long, могут обрабатываться
действительной основной памятью как два 32-битных
действия читать или записать, которые
могут разделены во времени с другими действиями,
приходящими между ними. Следовательно, если два
потока одновременно присваивают различные
значения на ту же разделяемую
переменную не volatile типа double или long,
последующее использование той переменной может
получить значение, которое не равно любой из
присваиваемых значений, но равно некоторой
смеси, зависящей от реализации, из двух значений.
Реализация свободна осуществить действия загружать,
сохранять, читать, и записать для
значений double или long
как атомарными действия с 64 битами; фактически,
это поощряется. Модель делит их в 32-битные
половины, ради некоторых популярных в настоящее
время микропроцессоров, которые будут не в
состоянии обеспечивать эффективные атомарные
операции памяти над 64-битными величинами. Это
было бы проще для языка Ява, определить все
операции памяти на отдельной переменной как
атомарной; более сложное определение -
прагматическая уступка текущей практики
аппаратных средств ЭВМ. В будущем эта уступка
может быть устранена. Тем временем,
программистов всегда предостерегают явно
синхронизированные доступы к разделяемым
переменным типа double и long.
Пусть T – поток, а L – замок. Существуют
некоторые ограничения на действия выполняемые T
относительно L:
- Действие блокировать T на L может
происходить только если для каждого потока S
отличного от T, число предшествующего
действия разблокировать S на L
равняется числу предшествующего действия блокировать
S на L. (Менее формально: только один поток
одновременно допускает предъявление требований
замку, и кроме того поток может приобретать один
и тот же замок много раз и не оставлять прав
собственности до соответствующего числа
выполняемых действий разблокировать .)
- Действие разблокировать потоком T на
замке L может происходить только если число
предшествующих действий разблокировать T
на L - строго меньше чем число предшествующих
действий блокировать T на L. (Менее
формально: поток не разрешает разблокировать
замок .)
Относительно замка, действия блокировать и разблокировать,
выполняемые всеми потоками выполняются в
некотором порядке общем последовательном
порядке. Этот общий порядок должен быть
согласован с общим порядком действий каждого
потока.
Пусть T - какой-нибудь поток, пусть V -
какая-нибудь переменная, и пусть L - некоторый
замок. Имеются ограничения на действия,
выполняемые T относительно V и L:
- Между действием присваивать T на V и
последующим действием разблокировать T
на L, должно вмешиваться действие сохранить
T на V ; кроме того, действие записать
соответствующее действию сохранить, должно предшествовать действию разблокировать,
как видим основной памятью. (Менее формально:
если поток должен исполнить действие разблокировать
на любом замке, то это должно сначала копии всех
присвоенных значений рабочей памяти возвратить
основной памяти.)
- Между действием загружать T на L и
последующим действием использовать или сохранять
T на V, должно вмешиваться действие присваивать
или загружать на V; кроме того, если это
действие - загружать, тогда действие читать,
соответствующее тому действию загружать
должно следовать за действием блокировать,
как видно с помощью основной памяти. (Менее
формально: действие блокировать действует
как все текущие переменные из потока рабочей
памяти; перед использованием они должны
присвоены или загружены из основной памяти.)
Если переменная объявлена volatile,
тогда дополнительные ограничения применяются к
действиям каждого потока. Пусть T - поток и
пусть V и W - переменные volatile.
- Действие использовать T на V
разрешается только, если предыдущее действие T
на V было загружать, и действие загружать
T на V разрешается только если следующее
действие T на V является использовать.
Действие использовать, как говорится, "связано“ с действием читать
которое соответствует действию загружать.
- Действие сохранять T на V разрешается
только, если предыдущее действие T на V
было присваивать, и действие присваивать T
на V разрешается только если следующее
действие T на V - сохранять. Действие присваивать,
как сказано, "связано” с
действием записать, которое соответствует
действию сохранять.
- Пусть А - действие использовать или присваивать
потоком T на переменную V, пусть F
действие загружать или сохранять,
связанное с A, и пусть P действие читать
или записать V , соответствующее F.
Подобно этому, пусть B действие использовать
или присваивать потоком T на переменную W,
пусть G действие загружать или сохранять,
связанное с B, и пусть Q действие читать
или записать V, соответствующее G. Если
А предшествует B, тогда P должно
предшествовать Q. (Менее формально: действия
на оригиналах переменных типа volatile
от имени потока исполняются основной памятью в
точном порядке, который требует поток.)
Если переменная не объявлена volatile,
то правила в предыдущих частях менее строгие,
чтобы позволить действиям сохранять произойти
раньше, чем было разрешено. Цель этой “не строгости”
состоит в том, чтобы позволить оптимизировать
компиляторы языка Ява выполняющие некоторые
виды перестановки кода, которые сохраняют
семантику должным образом синхронизированных
программ, но могли бы быть пойманы на месте
выполнения действий памятью в порядке, в
соответствии с программами, которые должным
образом не синхронизированы.
Представьте, что сохранить T V следует за
действием присваивать T V согласно
правилам предыдущих частей, без вмешивающегося действия загружать или присваивать
T V. Тогда действие сохранять послало бы
основной памяти значение которое действие присваивать
поместит в рабочую память потока T.
Специальное правило позволяет действию сохранять
происходить перед действием присваивать,
если выполняются следующие ограничения:
- Если происходит действие сохранять, то
действие присваивать сдерживается. (Помните, что
это ограничения на то, что фактически случается,
но не на то что планирует делать поток. Нет
выполнения действия сохранять и генерирования
исключений перед действием присваивать!)
- Никакое действие блокировать не
вмешивается между перерасположенными
действиями сохранять и присваивать.
- Никакое действие загружать V не вмешивается
между перерасположенными действиями сохранять
и присваивать.
- Никакое другое действие сохранять V не
вмешивается между перерасположенными
действиями сохранять и присваивать.
- Действие сохранять посылает основной памяти
значение, которое действие присваивать будет
помещать в рабочую память потока T.
Эта последнее свойство вынуждает нас назвать
такое раннее действие сохранять опережающим: так или иначе это должно быть
известно раньше времени, какое значение будет
сохранено действием присваивать и что должно
следовать. Практически, оптимизированный
компилируемый код вычислит такие значения
раньше( который разрешается, если, например,
вычисление не имеет никакого влияния со стороны
и не генерирует исключения), сохранит их раньше
(при входе в цикл, например), и хранят их в рабочих
регистрах для более позднего использования в
пределах цикла.
Любая связь между замками и переменными вполне
обычна. Блокирование любого замка как бы
изолирует все переменные из рабочей памяти
потока, а разблокирование любого замка вынуждает
записывать в основную память все переменные,
которым потоком присвоено значение. То, что замок
может быть связан с отдельным объектом или
классом - просто соглашение. В некоторых
приложениях так может быть всегда, для того,
например, чтобы блокировать объект при доступе к
любой переменной экземпляра;
синхронизированные методы - удобный способ
следовать этим соглашением. В других
приложениях, может использоваться один замок,
чтобы синхронизировать доступ к большому
количеству объектов.
Если поток использует некоторую разделяемую
переменную только после блокирования некоторого
замка и перед разблокированием этого же замка,
тогда поток будет читать разделяемое значение
этой переменной из основной памяти после
действия блокировать, если необходимо, и
копировать назад в основную память значение,
недавно присвоенное этой переменной перед
действием разблокировать. Это, в соединении с
правилами взаимного исключения для замков,
гарантирует что значения правильно переданы от
одного потока другому через разделяемые
переменные.
Правила для переменных volatile
требуют, чтобы основная память была
задействована только один раз для каждого
действия использовать или присваивать, выполняемым
потоком для переменной volatile, и
что основная память, воздействует именно
в порядке, продиктованным семантикой выполнения
потока. Однако, такие действия памяти не
упорядочиваются относительно действий читать
и записать на переменных volatile.
Рассмотрим класс, который имеет переменные
класса a и b и
методы hither и yon:
class Sample {
int a = 1, b = 2;
void hither() {
a = b;
}
void yon() {
b = a;
}
}
Пусть созданы два потока, и пусть один поток
вызывает hither в то время как другой
поток вызывает yon. Каков
необходимый набор действий и каков порядок
применения ограничений?
Рассмотрим поток, который вызывает hither.
Согласно правилам, этот поток должен исполнить
действие использовать b,
сопровождаемого действием присваивать a. Это - достаточный минимум,
необходимый чтобы выполнить
вызов метода hither.
Первое действие на переменной b
выполняемое потоком не может быть действием использовать.
Но может быть действием присваивать или загружать.
Действие присваивать b не
может происходить, потому что текст программы не
вызывает такого действия, так что требуется
действие загружать b. Это
действие загружать выполняемое потоком в
свою очередь требует предшествующего действия читать
для b основной памяти.
Поток может необязательно сохранять
значение после того, как произошло действие присваивать.
Если это выполняется, тогда действие сохранять,
в свою очередь, требует, чтобы следующим было
действие записать основной памяти.
Ситуация для потока, который вызывает yon - подобна, но роли а
и b меняются местами.
Общий набор действий может быть изображен
следующим образом:
поток hither основная память
поток yon
читать b читать a
загружать b загружать a
использовать b использовать a
присваивать a
присваивать b
[ сохранять a ] [ сохранять
b ]
[ записать a ] [ записать b ]
Здесь стрелка от действия А к действию B
указывает, что A должно идти перед B.
В каком порядке могут происходить действия
основной памяти? Единственное ограничение
состоит в том, что не возможно, чтобы действие записать
a, предшествовало действию читать
a и чтобы действие записать b, предшествовало действию читать
b, потому что стрелки в схеме образовали бы петлю
так, что действие было бы должно предшествовать
себе, что не допустимо. Предположение, что
необязательные действия сохранять и записать
должны происходить, там где есть три возможных
последовательности. И три эти
последовательности Вы и можете видеть
несколькими строками ниже в которых основная
память могла бы правильно исполнять эти
действия. Пусть ha и hb
будут рабочими копиями a и b для потока hither, пусть
ya и yb будут
рабочими копиями для потока yon, и
пусть ma и mb будут
мастер-копиями в основной памяти.
Первоначально ma=1
и mb=2. Тогда возможны следующие три
последовательности действий и результирующих
состояний:
- записать a® читать
a, читать b® записать b
(тогда ha=2, hb=2, ma=2, mb=2, ya=2, yb=2)
- читать a® записать
a, записать b® читать b (тогда
ha=1, hb=1, ma=1, mb=1, ya=1, yb=1)
- читать a® записать a, читать b®
записать b (тогда ha=2, hb=2, ma=2,
mb=1, ya=1, yb=1)
Таким образом точным результатом могло бы быть
то, что, в основной памяти b скопировано
в a, a скопировано в b,
или значения a и b -
обмениваются; кроме того, рабочие копии
переменных могут быть согласованными или могут
не быть согласованными. Это было бы некорректно,
принимать какой-нибудь из этих результатов более
вероятным чем другой. Это единственное место в
котором поведение программы на языке Ява -
обязательно зависимо от времени.
Конечно, реализация могла бы также не выполнять
действия хранить и записать, или только
одно из них, что приводит к другим
возможным результатам.
Теперь представьте, что мы изменяем пример,
чтобы использовать методы synchronized:
class SynchSample {
int a = 1, b = 2;
synchronized void hither() {
a = b;
}
synchronized void yon() {
b = a;
}
}
Вновь рассмотрим поток, который вызывает hither. Согласно правилам, этот поток
должен выполнить действие блокировать
(объект класса для класса SynchSample)
прежде чем тело метода hither будет
выполнено. Это сопровождается действием использовать
b и затем действием присваивать a. Наконец, действие разблокировать
объект класса должно быть выполнено после
выполнения тела метода hither. Это -
достаточный минимум, необходимый, чтобы
выполнить вызов метода hither.
Как и прежде, требуется действие загружать b, которое в свою очередь требует
предшествующего действия читать b
основной памятью. Поскольку действие загружать
следует за действием блокировать,
соответствующее действие читать должно
также следовать за действием блокировать.
Поскольку действие разблокировать следует
за действием присваивать a,
действие сохранить a, которое
в свою очередь требует, чтобы следующее действие
было записать для a основной
памяти. Действие записать должно
предшествовать действию разблокировать.
Ситуация для потока, который вызывает yon, подобна, но роли а и b меняются местами.
Общий набор действий может быть изображен
следующим образом:
поток hither основная память поток yon
блокировать класс SynchSample
блокировать класс SynchSample
читать b читать a
загружать b
загружать a
использовать b использовать
a
присваивать a присваивать
b
сохранить a сохранить b
записать a записать b
разблокировать класс SynchSample разблокировать
класс SynchSample
Действия блокировать и разблокировать
предусматривают дальнейшие ограничения на
порядок действий основной памяти; действие блокировать
одним потоком не может происходить между
действиями блокировать и разблокировать
другим потоком. Кроме того, действие разблокировать
требует чтобы происходили действия хранить и
записать. Из этого следует, что возможны
только две последовательности:
- записать a® читать
a, читать b® записать b
(тогда ha=2, hb=2, ma=2, mb=2, ya=2, yb=2)
- читать a® записать
a, записать b® читать b
(тогда ha=1, hb=1, ma=1, mb=1, ya=1, yb=1)
В то время пока результат состояния зависит от
времени, можно заметить, что два потока
обязательно будут “согласовываться” со
значениями a и b.
Этот пример подобен примеру предшествующей
части, за исключением того, что один метод
присваивает обе переменные, и другой метод
читает обе переменные. Рассмотрим класс, который
имеет переменные класса a и b и методы to и fro:
class Simple {
int a = 1, b = 2;
void to() {
a = 3;
b = 4;
}
void fro() {
System.out.println("a= " + a + ", b=" + b);
}
}
Теперь представьте, что созданы два потока, и
что один поток вызывает to в то
время как другой поток вызывает fro.
Каков требуемый набор действий и какое
упорядочение ограничений?
Рассмотрим поток, который вызывает to.
Согласно правилам, этот поток должен исполнить
действие присваивать a,
следующего за действием присваивать b. Это - достаточный и необходимый
минимум выполнения вызова метода to.
Поскольку нет никакой синхронизации, это - выбор
реализации хранить или не хранить присваиваемые
значения обратно в основной памяти! Поэтому
поток, который вызывает fro, может
получить или 1 или 3
для значения a, и независимо может
получить или 2 или 4
для значения b.
Представьте, что to
синхронизирован, но fro - нет:
class SynchSimple {
int a = 1, b = 2;
synchronized void to() {
a = 3;
b = 4;
}
void fro() {
System.out.println("a= " + a + ", b=" + b);
}
}
В этом случае метод to вынужден хранить
присваиваемые значения обратно в основной
памяти перед действием разблокировать в
конце метода. Метод fro должен,
конечно, использовать а и b (в этом порядке) и должен загружать
значения для а и b из
основной памяти.
Общий набор действий может быть изображен
следующим образом:
поток to основная память поток fro
читать a читать b
блокировать класс SynchSimple загружать
a загружать b
присваивать a использовать
a
присваивать b использовать
b
сохранить a сохранить b печать
записать a записать b
разблокировать класс SynchSimple
Здесь стрелка от действия A к действию B
указывает, что A должно идти перед B.
В каком порядке могут происходить действия
основной памяти? Заметьте, что правила не требуют
действие записать а
происходить прежде, чем действие записать b; они также не требуют чтобы действие
читать а происходило прежде,
чем - читать b. Даже если метод to синхронизирован, а метод fro не синхронизирован, то ничего не
может предотвратить действие читать от
появления между действиями блокировать и разблокировать.
(Дело в том , что объявление одного метода synchronized еще не
означает, что метод ведет себя, как атомарный.)
В результате, метод fro мог бы все
еще получить из а значение или 1 или 3, и независимо
мог бы получить из b значение или 2 или 4. В частности, fro мог бы наблюдать значение 1 для a и 4
для b. Таким
образом, даже если to делает
действие присваивать а и
затем присваивать b, действия записать
в основную память могут наблюдаться другим
потоком, т.е. происходить как
будто бы в обратном порядке.
Наконец, представьте, что to и fro синхронизированы:
class SynchSynchSimple {
int a = 1, b = 2;
synchronized void to() {
a = 3;
b = 4;
}
synchronized void fro() {
System.out.println("a= " + a + ", b=" + b);
}
}
В этом случае, действия метода fro
не могут чередоваться с действиями метода to, и fro будет печатать
или "а=1, b=2" или "a=3, b=4".
Потоки создаются и управляются встроенными
классами Thread (§ 20.20) и ThreadGroup
(§ 20.21).
Создание объекта Thread создает
поток, и это является единственным способом
создания потока. Когда поток создан, он еще не
активен; он начинает работать, когда вызывается
его метод start (§ 20.20.14).
Каждый поток имеет приоритет. Когда
возникает соревнование за обработку ресурсов,
потоки с более высоким приоритетом вообще
выполняются вместо потоков с более низким
приоритетом. Такого предпочтения нет, однако,
гарантия того что поток с самым высоким
приоритетом будет всегда происходить, и
приоритеты потоков не могут использоваться,
чтобы надежно реализовать взаимное исключение.
Существует замок, связанный с каждым объектом.
Язык Ява не обеспечивает способ исполнения
отдельных действий блокировать и разблокировать;
вместо этого они неявно реализуют с помощью
конструкции высокого уровня, которые всегда
договариваются, чтобы соединить такие действия
правильно. (Однако мы замечаем, что виртуальная
машина языка Ява обеспечивает отдельный вход в
монитор и выход из монитора инструкции,
которые реализуют действия блокировать и разблокировать.)
Оператор synchronized (§
14.17) вычисляет ссылку на объект; затем пытается
исполнять действие блокировать на этом
объекте и не переходить далее до успешно
выполненного действия блокировать. (Действие
блокировать может быть отсрочено, потому что
правила относительно замков могут предотвращать
основную память от участия в некотором другом
потоке готового исполнить одно или большое
количество действий разблокировать.) После
того, как выполнено действие блокировать,
выполняется тело оператора synchronized.
Если выполнение тела когда-либо заканчивается,
или нормально или преждевременно, автоматически
выполняется действие разблокировать на этом
же самом замке.
Метод synchronized (§
8.4.3.5) автоматически выполняет действие блокировать,
когда он вызван; тело метода не выполняется, пока
не завершится действие блокировать. Если
метод является методом экземпляра, то он
блокирует замок, связанный с экземпляром для
которого он вызван (то есть объект, который будет
известен как this в течение
выполнения тела метода). Если метод static,
то он блокирует , связанный с объектом Class,
представляет класс, в котором определен метод.
Если выполнение тела метода когда-либо
заканчивается, нормально или преждевременно,
действие разблокировать выполняется
автоматически на этом же замке.
Лучшая практика в том что переменная
должна когда-либо быть присвоена одним потоком и
использоваться или присваиваться другим, тогда
все доступы к переменной должны быть включены в
методы synchronized или операторы synchronized.
Язык Ява не предотвращает, не требует
обнаружения условий взаимоблокировки.
Программы, где потоки содержат (непосредственно
или косвенно) замки на кратном числе объектов,
должны использовать обычные методы для
предотвращения тупика, создавая высокоуровневое
блокирование элементарных действий, которые
взаимно не блокируются, если это
необходимо.
Каждый объект в дополнение к наличию
связанного замка, имеет связанный набор
задержек, который является набором потоков.
Когда объект только создается, его набор
задержек пуст.
Наборы задержек используются методами wait (§ 20.1.6, § 20.1.7, § 20.1.8),
notify (§ 20.1.9), и notifyAll
(§ 20.1.10)
класса Object. Эти методы также
взаимодействуют c механизмом планирования для
потоков (§
20.20).
Метод wait нужно вызывать для
объекта только когда текущий поток (назовем его T)
блокируется замком объекта. Представьте, что
поток T фактически выполнил действия N блокировать, которые не
было согласованы действиями разблокировать.
Тогда метод wait добавляет текущий
поток к набору задержек для объекта, блокирует
текущий поток планирующим потоком и исполняет
действия N разблокировать, чтобы бросить
замок. Поток T находится в бездействии, пока
не происходит один из трех случаев:
- Некоторый другой поток вызывает метод notify для объекта и потока T,
случайно выбранного как один из объявленных.
- Некоторый другой поток призывает notifyAll
метод для объекта.
- Если вызов потоком T метода wait
определил задержку, указанное количество
реального времени задержки истекло.
Поток T тогда устранен из набора задержек
разрешил планирование потоков. Тогда он снова
блокирует объект (который может вызывать
конкуренцию как и обычно с другими потоками); как
только он получил контроль за
замком, он исполняет N-1 добавленные действия блокировать
и тогда возвращается из вызова метода wait.
Таким образом, возвращение из метода wait,
состояние замка объекта - точно,
поскольку это было когда метод wait был
вызван.
Метод notify вызывать для объекта
только, когда текущий поток уже блокирует объект.
Если набор задержек для объекта не пуст, тогда
некоторый произвольно выбранный поток устранен
из набора задержек и разрешил планирование
потоков. (Конечно, тот поток не способен
продолжать, пока текущий поток выпустит
блокировку объекта).
Метод notifyAll вызывать для объекта
только когда текущий поток уже блокирует объект.
Каждый поток в наборе задержек объекта удаляется
из набора задержек и повторно разрешает
планирование потока. (Конечно, потоки не будут
способны продолжать, пока текущий поток не
выпустит блокировку объекта.)
|