Spec-Zone .ru
спецификации, руководства, описания, API
|
Содержание | Предыдущий | Следующий | Индекс | Спецификация Виртуальной машины JavaTM |
ГЛАВА 8
Эта глава детализирует низкоуровневые действия, которые могут использоваться, чтобы объяснить взаимодействие потоков виртуальной машины Java с совместно используемой основной памятью. Это было переиздано с минимальными изменениями из Спецификации языка Java, Джеймсом Гослингом, Биллом Джоем, и Гаем Стилом.
У каждого потока есть рабочая память, в которой он сохраняет свою собственную рабочую копию переменных, которые он должен использовать или присвоить. Поскольку поток выполняет программу Java, он работает на этих рабочих копиях. Основная память содержит основную копию каждой переменной. Есть правила о том, когда поток разрешается или требуется передать содержание своей рабочей копии переменной в основную копию или наоборот.
Основная память также содержит блокировки; есть одна блокировка, связанная с каждым объектом. Потоки могут конкурировать, чтобы получить блокировку.
В целях этой главы, использования глаголов, присваивают, загружают, хранят, блокируют, и разблокировали действия имени, которые может выполнить поток. Глаголы читают, пишут, блокируют, и разблокировали действия имени, которые может выполнить основная подсистема памяти. Каждая из этих операций атомарная (неделимый).
Использование или присваивается, работа является сильно связанным взаимодействием между механизмом выполнения потока и рабочей памятью потока. Блокировка или разблокировала работу, сильно связанное взаимодействие между механизмом выполнения потока и основной памятью. Но передача данных между основной памятью и рабочей памятью потока слабо связывается. Когда данные копируются от основной памяти до рабочей памяти, два действия должны произойти: операция чтения, выполняемая основной памятью, сопровождаемой некоторое время спустя соответствующей работой загрузки, выполняется рабочей памятью. Когда данные копируются от рабочей памяти до основной памяти, два действия должны произойти: работа хранилища, выполняемая рабочей памятью, сопровождаемой некоторое время спустя соответствующей операцией записи, выполняется основной памятью. Может быть некоторое время транспортировки между основной памятью и рабочей памятью, и время транспортировки может отличаться для каждой транзакции; таким образом операции, инициируемые потоком на различных переменных, могут просматриваемый другим потоком как происходящий в различном порядке. Для каждой переменной, однако, операции в основной памяти от имени любого потока выполняются в том же самом порядке как соответствующие операции тем потоком. (Это объясняется более подробно позже.)
Единственный поток Java выпускает поток использования, присвойте, заблокируйте, и разблокируйте операции как диктующийся семантикой программы Java, которую это выполняет. Базовая реализация Java тогда требуется дополнительно выполнить соответствующую загрузку, хранилище, чтение, и операции записи, чтобы повиноваться определенному набору ограничений, объясненных позже. Если реализация Java правильно следует за этими правилами, и прикладной программист Java следует за определенными другими правилами программирования, то данные могут быть достоверно переданы между потоками через совместно используемые переменные. Правила разрабатываются, чтобы быть достаточно "трудными", чтобы сделать это возможным, но "освободить" достаточно, чтобы позволить аппаратным и программным разработчикам значительную свободу улучшить скорость и пропускную способность через такие механизмы как регистры, очереди, и кэши.
Вот подробные определения каждой из операций:
Потоки не взаимодействуют непосредственно; они связываются только через совместно используемую основную память. Отношения между действиями потока и действиями основной памяти ограничиваются тремя способами:
В правилах, которые следуют, формулировка "B должна вмешаться между A, и C" означает, что действие B должно следовать за действием A и предшествовать действию C.
+
оператор требует, чтобы единственная работа использования произошла на V; возникновение V как левый операнд оператора присваивания =
требует, чтобы сингл присвоился, работа происходят. Все использование и присваивается, действия данным потоком должны произойти в порядке, определенном программой, выполняемой потоком. Если следующие правила запрещают T выполнять необходимое использование в качестве своего следующего действия, может быть необходимо для T выполнить загрузку сначала, чтобы сделать успехи.
Есть также определенные ограничения на операции чтения и операции записи, выполняемые основной памятью:
volatile
переменные (§8.7). double
или long
переменная не объявляется volatile
, тогда в целях загрузки, хранилища, чтения, и операций записи это обрабатывается, как будто это были две переменные 32 битов каждый: везде, где правила требуют одной из этих операций, две таких операции выполняются, один для каждой 32-разрядной половины. Способ тот, в который 64 бита a double
или long
переменная кодируется в два 32-разрядных количества, и порядок операций на половинах переменных не определяются Спецификацией языка Java. Это имеет значение только потому, что чтение или запись a double
или long
переменная может быть обработана фактической основной памятью как две 32-разрядных операции чтения или операции записи, которые могут быть разделены вовремя с другими операциями, прибывающими между ними. Следовательно, если два потока одновременно присваивают отличные значения тому же самому, совместно использованному не -volatile
double
или long
переменная, последующее использование той переменной может получить значение, которое не равно любому из присвоенных значений, но небольшому количеству зависящей от реализации смеси двух значений.
Реализация свободна реализовать загрузку, хранилище, чтение, и операции записи для double
и long
значения как атомарные 64-разрядные операции; фактически, это строго поощряется. Модель делит их на 32-разрядные половины ради нескольких в настоящий момент популярных микропроцессоров, которые не в состоянии обеспечить эффективные атомарные транзакции памяти на 64-разрядных количествах. Было бы более просто для Java определить все транзакции памяти на единственных переменных как атомарные; это более сложное определение является прагматической концессией текущей аппаратной практике. В будущем может быть устранена эта концессия. Тем временем программистов предостерегают всегда явно синхронизировать доступ к совместно используемому double
и long
переменные.
volatile
, тогда правила в предыдущих разделах ослабляются немного, чтобы позволить операциям хранилища происходить ранее, чем было бы иначе разрешено. Цель этого расслабления состоит в том, чтобы позволить оптимизировать компиляторы Java, чтобы выполнить определенные виды перестановки кода, которые сохраняют семантику должным образом синхронизируемых программ, но могли бы быть пойманы выполнить операции памяти не в порядке программами, которые должным образом не синхронизируются. Предположите, что хранилище T V следовало бы, деталь присваиваются T V согласно правилам предыдущих разделов, без прошедшей загрузки или присваиваются T V. Затем та работа хранилища отправила бы основной памяти значение что присваивать работа, помещенная в рабочую память потока T. Специальное правило позволяет работе хранилища фактически происходить перед присваивать работой вместо этого, если следующим ограничениям повинуются:
synchronized
методы являются удобным способом следовать за этим соглашением. В других приложениях это может быть достаточным, чтобы использовать единственную блокировку, чтобы синхронизировать доступ к большому количеству объектов. Если поток будет использовать определенную совместно используемую переменную только после блокировки определенной блокировки и перед соответствующим разблокированием той же самой блокировки, то поток считает совместно используемое значение той переменной от основной памяти после работы блокировки, в случае необходимости, и скопирует назад в основную память значение, последний раз присвоенное той переменной перед разблокировать работой. Это, в соединении с правилами взаимного исключения для блокировок, достаточно, чтобы гарантировать, что значения правильно передаются от одного потока до другого через совместно используемые переменные.
Правила для энергозависимых переменных эффективно требуют, чтобы основная память была затронута точно однажды для каждого использования или присвоилась энергозависимой переменной потоком, и что основная память быть затронутой в точно порядке, продиктованном семантикой выполнения потока. Однако, такие операции памяти не упорядочиваются относительно операций чтения и операций записи на энергонезависимых переменных.
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
основной памятью.
Поток может дополнительно сохранить значение a
после того, как присваивание произошло. Если это делает, то работа хранилища поочередно требует следующей операции записи для a
основной памятью.
Ситуация для потока, который вызывает yon
подобно, но с ролями a
и b
обмененный.
Полный набор операций может быть изображен следующим образом:
Здесь стрелка от действия к действию B указывает, что Необходимость предшествует 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
подкачиваются; кроме того рабочие копии переменных могли бы или не могли бы согласиться. Было бы неправильно, конечно, предположить, что любой из этих результатов более вероятен чем другой. Это - одно место, в котором поведение программы Java обязательно зависимо от синхронизации. Конечно, реализация могла бы также хотеть не выполнять хранилище и операции записи, или только одну из этих двух пар, приводя все же к другим возможным результатам.
Теперь предположите, что мы изменяем пример, чтобы использовать synchronized
методы:
class SynchSample {
int a = 1, b = 2; synchronized void hither() { a = b; } synchronized void yon() b = a; } }Снова давайте рассматривать поток, который вызывает
hither
. Согласно правилам, этот поток должен выполнить работу блокировки (на Class
объект для класса SynchSample
) перед телом метода hither
выполняется. Это сопровождается использованием b
и затем присваивание a
. Наконец, разблокировать работа на Class
объект должен быть выполнен после тела метода hither
завершается. Это - пустой минимум, требуемый выполнить звонок в метод hither
. Как прежде, загрузка b
требуется, который поочередно требует предыдущей операции чтения для b
основной памятью. Поскольку загрузка следует за работой блокировки, соответствующее чтение должно также следовать за работой блокировки.
Поскольку разблокировать работа следует за присваиванием a
, работа хранилища на a
обязательно, который поочередно требует следующей операции записи для a
основной памятью. Запись должна предшествовать разблокировать работе.
Ситуация для потока, который вызывает yon
подобно, но с ролями a
и b
обмененный.
Полный набор операций может быть изображен следующим образом:
Блокировка и разблокировала операции, обеспечивают дальнейшие ограничения на порядок операций основной памятью; работа блокировки одним потоком не может произойти между блокировкой и разблокировать операции другого потока. Кроме того разблокировать операции требуют, чтобы хранилище и операции записи произошли. Из этого следует, что только две последовательности возможны:
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
synchronized
но 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
должен, конечно, использовать a
и b
(в том порядке), и так должен загрузить значения для a
и b
от основной памяти. Полный набор операций может быть изображен следующим образом:
Здесь стрелка от действия к действию B указывает, что Необходимость предшествует B.
В каком порядок может операции основной памятью происходить? Отметьте, что правила не требуют той записи a
происходите перед записью b
; и при этом они не требуют того чтения a
происходите прежде, чем считано b
. Кроме того, даже при том, что метод to
синхронизируется, метод fro
не синхронизируется, таким образом нет ничего, чтобы препятствовать тому, чтобы операции чтения произошли между блокировкой и разблокировали операции. (Дело в том, что объявление одного метода synchronized
не делает себя, заставляют тот метод вести себя, как будто это было атомарным.)
В результате метод fro
мог все еще получить также 1
или 3
для значения a
, и независимо мог получить также 2
или 4
для значения b
. В частности fro
мог бы наблюдать значение 1
для a
и 4
для b
. Таким образом, даже при том, что to
делает присваивание a
и затем присваивание b
, операции записи к основной памяти, как может наблюдать другой поток, происходят как будто в противоположном порядке.
Наконец, предположите это to
и fro
оба synchronized
:
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
напечатает любого"a=1, b=2
"или"a=3, b=4
".Thread
и ThreadGroup
. Создание a Thread
объект создает поток, и это - единственный способ создать поток. Когда поток создается, это еще не активно; это начинает работать когда start
метод вызывают. synchronized
оператор вычисляет ссылку на объект; это тогда пытается выполнить работу блокировки на том объекте и не продолжается далее, пока работа блокировки успешно не завершилась. (Работа блокировки может быть задержана, потому что правила о блокировках могут препятствовать тому, чтобы основная память участвовала, пока некоторый другой поток не готов выполнить один, или больше разблокировало операции.) После того, как работа блокировки была выполнена, тело synchronized
оператор выполняется. Если выполнение тела когда-либо завершается, или обычно или резко, разблокировать работа автоматически выполняется на той же самой блокировке.
A synchronized
метод автоматически выполняет работу блокировки, когда он вызывается; его тело не выполняется, пока работа блокировки успешно не завершилась. Если метод является методом экземпляра, он блокирует блокировку, связанную с экземпляром, для которого он был вызван (то есть, объект, который будет известен как this
во время выполнения тела метода). Если метод static
, это блокирует блокировку, связанную с Class
объект, который представляет класс, в котором определяется метод. Если выполнение тела метода когда-либо завершается, или обычно или резко, разблокировать работа автоматически выполняется на той же самой блокировке.
Передовая практика то, что, если переменная должна когда-либо присваиваться одним потоком и использоваться или присваиваться другим, то во все доступы к той переменной нужно включить synchronized
методы или synchronized
операторы.
Ожидайте наборы используются методами wait
, notify
, и notifyAll
из класса Object
. Эти методы также взаимодействуют с механизмом планирования для потоков.
Метод wait
должен быть вызван для объекта только, когда текущий поток (вызывают это T) уже заблокировал блокировку объекта. Предположите, что поток T фактически выполнил операции блокировки N, которые не были соответствующими, разблокировали операции. wait
метод тогда добавляет текущий поток к ожидать набору для объекта, отключает текущий поток в целях планирования потоков, и выполняет N, разблокировали операции, чтобы оставить блокировку. Поток T тогда бездействует, пока одна из трех вещей не происходит:
notify
метод для того объекта, и поток T, оказывается, тот, произвольно выбранный в качестве того, чтобы уведомить.
notifyAll
метод для того объекта.
wait
метод, определенный интервал тайм-аута, тогда указанное количество реального времени, протек. wait
метод. Таким образом, по возврату из wait
метод, состояние блокировки объекта точно, как это было когда wait
метод был вызван.
notify
метод должен быть вызван для объекта только, когда текущий поток уже заблокировал блокировку объекта, или IllegalMonitorState
-Exception
будет брошен. Если ожидать набор для объекта не пуст, то некоторый произвольно выбранный поток удаляется из ожидать набора и повторно включается для планирования потоков. (Конечно, тот поток не будет в состоянии продолжиться, пока текущий поток не оставляет блокировку объекта.)
notifyAll
метод должен быть вызван для объекта только, когда текущий поток уже заблокировал блокировку объекта, или IllegalMonitorState
Exception
будет брошен. Каждый поток в ожидать наборе для объекта удаляется из ожидать набора и повторно включается для планирования потоков. (Конечно, те потоки не будут в состоянии продолжиться, пока текущий поток не оставляет блокировку объекта.)
Содержание | Предыдущий | Следующий | Индекс
Спецификация Виртуальной машины Java
Авторское право © 1996, 1997 Sun Microsystems, Inc. Все права защищены
Пожалуйста, отправьте любые комментарии или исправления к jvm@java.sun.com