Spec-Zone .ru
спецификации, руководства, описания, API
|
ГЛАВА 17
В то время как большая часть обсуждения в предыдущих главах затрагивается только с поведением кода Java как выполняющийся единственный оператор или выражение за один раз, то есть, единственным потоком, каждая виртуальная машина Java может поддерживать много потоков выполнения сразу. Эти потоки независимо выполняют код Java, который работает на значениях Java и объектах, находящихся в совместно используемой основной памяти. Потоки могут поддерживаться при наличии многих аппаратных процессоров квантованием времени единственный аппаратный процессор, или квантованием времени много аппаратных процессоров.
Java поддерживает кодирование программ, которые, хотя параллельный, все еще показывают детерминированное поведение, обеспечивая механизмы для того, чтобы они синхронизировали параллельное действие потоков. Чтобы синхронизировать потоки, Java использует мониторы, которые являются высокоуровневым механизмом для того, чтобы позволить только одному потоку за один раз выполнять область кода, защищенного монитором. Поведение мониторов объясняется с точки зрения блокировок; есть блокировка, связанная с каждым объектом.
synchronized
оператор (§14.17) выполняет два специальных действия, относящиеся только к многопоточной работе: (1) после вычислений ссылки на объект, но прежде, чем выполнить его тело, это блокирует блокировку, связанную с объектом, и (2) после того, как выполнение тела завершилось, или обычно или резко, это разблокировало ту же самую блокировку. Как удобство, может быть объявлен метод synchronized
; такой метод ведет себя, как будто его тело содержалось в a synchronized
оператор.
Методы wait
(§20.1.6, §20.1.7, §20.1.8), notify
(§20.1.9), и notifyAll
(§20.1.10) класса Object
поддерживайте эффективную передачу управления от одного потока до другого. Вместо того, чтобы просто "вращаться" (неоднократно блокировка и разблокирование объекта видеть, изменилось ли некоторое внутреннее состояние), который использует вычислительное усилие, поток может приостановить себя использование wait
до тех пор, пока другой поток пробуждает это использование notify
. Это является особенно соответствующим в ситуациях, где у потоков есть отношение производителя-потребителя (активно сотрудничающий на общей цели), а не отношение взаимного исключения (пытающийся избежать конфликтов, совместно используя общий ресурс).
Поскольку поток выполняет код, он выполняет последовательность действий. Поток может использовать значение переменной или присвоить его новое значение. (Другие действия включают арифметические операции, условные тесты, и вызовы метода, но они не делают включает переменные непосредственно.), Если два или больше параллельных действия потоков на совместно используемой переменной, есть возможность, что действия на переменной приведут к зависимым от синхронизации результатам. Эта зависимость от синхронизации свойственна от параллельного программирования, производя одно из немногих мест в Java, где результат выполнения программы не определяется исключительно этой спецификацией.
У каждого потока есть рабочая память, в которой он может сохранить копии значений переменных от основной памяти, которая совместно используется всеми потоками. Чтобы получить доступ к совместно используемой переменной, поток обычно сначала получает блокировку и сбрасывает ее рабочую память. Это гарантирует, что совместно использованные значения будут после того быть загруженными от совместно используемой основной памяти до потоков рабочая память. То, когда поток разблокирует блокировку, он гарантирует значения, которые он содержит в ее рабочей памяти, будет записано обратно к основной памяти.
Эта глава объясняет взаимодействие потоков с основной памятью, и таким образом друг с другом, с точки зрения определенных низкоуровневых действий. Есть правила о порядке, в котором могут произойти эти действия. Эти правила налагают ограничения на любую реализацию Java, и программист Java может положиться на правила предсказать возможные поведения параллельной программы Java. Правила действительно, однако, преднамеренно дают конструктору определенные свободы; намерение состоит в том, чтобы разрешить аппаратные и программные методы определенного стандарта, которые могут значительно улучшить скорость и эффективность параллельного кода.
Кратко помещенный, они - важные последствия правил:
long
и double
значения; см. §17.4.)
У каждого потока есть рабочая память, в которой он сохраняет свою собственную рабочую копию переменных, которые он должен использовать или присвоить. Поскольку поток выполняет программу Java, он работает на этих рабочих копиях. Основная память содержит основную копию каждой переменной. Есть правила о том, когда поток разрешается или требуется передать содержание своей рабочей копии переменной в основную копию или наоборот
Основная память также содержит блокировки; есть одна блокировка, связанная с каждым объектом. Потоки могут конкурировать, чтобы получить блокировку.
В целях этой главы, использования глаголов, присваивают, загружают, хранят, блокируют, и разблокировали действия имени, которые может выполнить поток. Глаголы читают, пишут, блокируют, и разблокировали действия имени, которые может выполнить основная подсистема памяти. Каждое из этих действий атомарное (неделимый).
Использование или присваивается, действие является сильно связанным взаимодействием между механизмом выполнения потока и рабочей памятью потока. Блокировка или разблокировала действие, сильно связанное взаимодействие между механизмом выполнения потока и основной памятью. Но передача данных между основной памятью и рабочей памятью потока слабо связывается. Когда данные копируются от основной памяти до рабочей памяти, два действия должны произойти: действие чтения, выполняемое основной памятью, сопровождаемой некоторое время спустя соответствующим действием загрузки, выполняется рабочей памятью. Когда данные копируются от рабочей памяти до основной памяти, два действия должны произойти: действие хранилища, выполняемое рабочей памятью, сопровождаемой некоторое время спустя соответствующим действием записи, выполняется основной памятью. Может быть некоторое время транспортировки между основной памятью и рабочей памятью, и время транспортировки может отличаться для каждой транзакции; таким образом действия, инициируемые потоком на различных переменных, могут просматриваемый другим потоком как происходящий в различном порядке. Для каждой переменной, однако, действия в основной памяти от имени любого потока выполняются в том же самом порядке как соответствующие действия тем потоком. (Это объясняется более подробно ниже.)
Единственный поток Java выпускает поток использования, присвойте, заблокируйте, и разблокируйте действия как диктующийся семантикой программы Java, которую это выполняет. Базовая реализация Java тогда требуется дополнительно выполнить соответствующую загрузку, хранилище, чтение, и действия записи, чтобы повиноваться определенному набору ограничений, объясненных ниже. Если реализация Java правильно следует за этими правилами, и прикладной программист Java следует за определенными другими правилами программирования, то данные могут быть достоверно переданы между потоками через совместно используемые переменные. Правила разрабатываются, чтобы быть достаточно "трудными", чтобы сделать это возможным, но "освободить" достаточно, чтобы позволить аппаратным и программным разработчикам значительную свободу улучшить скорость и пропускную способность через такие механизмы как регистры, очереди, и кэши.
Вот подробные определения каждого из действий:
Потоки не взаимодействуют непосредственно; они связываются только через совместно используемую основную память. Отношения между действиями потока и действиями основной памяти ограничиваются тремя способами:
В правилах, которые следуют, формулировка "B должна вмешаться между A, и C" означает, что действие B должно следовать за действием A и предшествовать действию C.
+
оператор требует, чтобы единственное действие использования произошло на V; возникновение V как левый операнд оператора присваивания =
требует, чтобы сингл присвоился, действие происходят. Все использование и присваивается, действия данным потоком должны произойти в порядке, определенном программой, выполняемой потоком. Если следующие правила запрещают T выполнять необходимое использование в качестве своего следующего действия, может быть необходимо для T выполнить загрузку сначала, чтобы сделать успехи.
Есть также определенные ограничения на чтение и действия записи, выполняемые основной памятью:
volatile
переменные (§17.7). double
и long
double
или long
переменная не объявляется volatile
, тогда в целях загрузки, хранилища, чтения, и действий записи они обрабатываются, как будто они были двумя переменными 32 битов каждый: везде, где правила требуют одного из этих действий, два таких действия выполняются, один для каждой 32-разрядной половины. Способ тот, в который 64 бита a double
или long
переменная кодируется в два 32-разрядных количества, является зависящим от реализации. Это имеет значение только потому, что чтение или запись a double
или long
переменная может быть обработана фактической основной памятью как два 32-разрядных чтения или действия записи, которые могут быть разделены вовремя с другими действиями, прибывающими между ними. Следовательно, если два потока одновременно присваивают отличные значения тому же самому, совместно использованному не -volatile
double
или long
переменная, последующее использование той переменной может получить значение, которое не равно любому из присвоенных значений, но небольшому количеству зависящей от реализации смеси двух значений.
Реализация свободна реализовать загрузку, хранилище, чтение, и действия записи для double
и long
значения как атомарные 64-разрядные действия; фактически, это строго поощряется. Модель делит их на 32-разрядные половины ради нескольких в настоящий момент популярных микропроцессоров, которые не в состоянии обеспечить эффективные атомарные транзакции памяти на 64-разрядных количествах. Было бы более просто для Java определить все транзакции памяти на единственных переменных как атомарные; это более сложное определение является прагматической концессией текущей аппаратной практике. В будущем может быть устранена эта концессия. Тем временем программистов предостерегают всегда явно синхронизировать доступ к совместно используемому double
и long
переменные.
volatile
, тогда дополнительные ограничения применяются к действиям каждого потока. Позвольте T быть потоком и позволять V и W быть энергозависимыми переменными. volatile
, тогда правила в предыдущих разделах ослабляются немного, чтобы позволить действиям хранилища происходить ранее, чем было бы иначе разрешено. Цель этого расслабления состоит в том, чтобы позволить оптимизировать компиляторы Java, чтобы выполнить определенные виды перестановки кода, которые сохраняют семантику должным образом синхронизируемых программ, но могли бы быть пойманы выполнить действия памяти не в порядке программами, которые должным образом не синхронизируются. Предположите, что хранилище T V следовало бы, деталь присваиваются T V согласно правилам предыдущих разделов, без прошедшей загрузки или присваиваются T V. Затем то действие хранилища отправило бы основной памяти значение что присваивать действие, помещенное в рабочую память потока T. Специальное правило позволяет действию хранилища вместо этого происходить перед присваивать действием, если следующим ограничениям повинуются:
Если поток будет использовать определенную совместно используемую переменную только после блокировки определенной блокировки и перед соответствующим разблокированием той же самой блокировки, то поток считает совместно используемое значение той переменной от основной памяти после действия блокировки, в случае необходимости, и скопирует назад в основную память значение, последний раз присвоенное той переменной перед разблокировать действием. Это, в соединении с правилами взаимного исключения для блокировок, достаточно, чтобы гарантировать, что значения правильно передаются от одного потока до другого через совместно используемые переменные.
Правила для volatile
переменные эффективно требуют, чтобы основная память была затронута точно однажды для каждого использования или присвоилась a 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
основной памятью.
Поток может дополнительно сохранить значение 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
. Согласно правилам, этот поток должен выполнить действие блокировки (на объекте класса для класса SynchSample
) перед телом метода hither
выполняется. Это сопровождается использованием b
и затем присваивание a
. Наконец, разблокировать действие на объекте класса должно быть выполнено после тела метода 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
synchronized
, метод fro
не synchronized
, таким образом, нет ничего, чтобы препятствовать тому, чтобы действия чтения произошли между блокировкой и разблокировали действия. (Дело в том, что объявление одного метода 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
(§20.20) и ThreadGroup
(§20.21). Создание a Thread
объект создает поток, и это - единственный способ создать поток. Когда поток создается, это еще не активно; это начинает работать когда start
метод (§20.20.14) вызывают. У каждого потока есть приоритет. Когда есть соревнование за обработку ресурсов, потоки с более высоким приоритетом обычно выполняются в предпочтении к потокам с более низким приоритетом. Такое предпочтение не является, однако, гарантией, что самый высокий приоритетный поток будет всегда работать, и распараллеливают приоритеты, не может использоваться, чтобы достоверно реализовать взаимное исключение.
synchronized
оператор (§14.17) вычисляет ссылку на объект; это тогда пытается выполнить действие блокировки на том объекте и не продолжается далее, пока действие блокировки успешно не завершилось. (Действие блокировки может быть задержано, потому что правила о блокировках могут препятствовать тому, чтобы основная память участвовала, пока некоторый другой поток не готов выполнить один, или больше разблокировало действия.) После того, как действие блокировки было выполнено, тело synchronized
оператор выполняется. Если выполнение тела когда-либо завершается, или обычно или резко, разблокировать действие автоматически выполняется на той же самой блокировке.
A synchronized
метод (§8.4.3.5) автоматически выполняет действие блокировки, когда он вызывается; его тело не выполняется, пока действие блокировки успешно не завершилось. Если метод является методом экземпляра, он блокирует блокировку, связанную с экземпляром, для которого он был вызван (то есть, объект, который будет известен как this
во время выполнения тела метода). Если метод static
, это блокирует блокировку, связанную с Class
объект, который представляет класс, в котором определяется метод. Если выполнение тела метода когда-либо завершается, или обычно или резко, разблокировать действие автоматически выполняется на той же самой блокировке.
Передовая практика то, что, если переменная должна когда-либо присваиваться одним потоком и использоваться или присваиваться другим, то во все доступы к той переменной нужно включить synchronized
методы или synchronized
операторы.
Java не предотвращает, ни требует обнаружения, условия мертвой блокировки. Программы, где потоки содержат (прямо или косвенно), соединяются, многократные объекты должны использовать стандартные методы для предотвращения мертвой блокировки, создавая высокоуровневые примитивы блокировки, которые не делают мертвой блокировки в случае необходимости.
Ожидайте наборы используются методами wait
(§20.1.6, §20.1.7, §20.1.8), notify
(§20.1.9), и notifyAll
(§20.1.10) класса Object
. Эти методы также взаимодействуют с механизмом планирования для потоков (§20.20).
Метод wait
должен быть вызван для объекта только, когда текущий поток (вызывают это T) уже заблокировал блокировку объекта. Предположите, что поток T фактически выполнил действия блокировки N, которые не были соответствующими, разблокировали действия. wait
метод тогда добавляет текущий поток к ожидать набору для объекта, отключает текущий поток в целях планирования потоков, и выполняет N, разблокировали действия, чтобы оставить блокировку. Поток T тогда бездействует, пока одна из трех вещей не происходит:
notify
метод для того объекта и потока T, оказывается, тот, произвольно выбранный в качестве того, чтобы уведомить.
notifyAll
метод для того объекта.
wait
метод. Таким образом, по возврату из wait
метод, состояние блокировки объекта точно, как это было когда wait
метод был вызван. notify
метод нужно вызвать для объекта только, когда текущий поток уже заблокировал блокировку объекта. Если ожидать набор для объекта не пуст, то некоторый произвольно выбранный поток удаляется из ожидать набора и повторно включается для планирования потоков. (Конечно, тот поток не будет в состоянии продолжиться, пока текущий поток не оставляет блокировку объекта.)
notifyAll
метод нужно вызвать для объекта только, когда текущий поток уже заблокировал блокировку объекта. Каждый поток в ожидать наборе для объекта удаляется из ожидать набора и повторно включается для планирования потоков. (Конечно, те потоки не будут в состоянии продолжиться, пока текущий поток не оставляет блокировку объекта.)
Содержание | Предыдущий | Следующий | Индекс
Спецификация языка Java (HTML, сгенерированный Блинчиком "сюзет" Pelouch 24 февраля 1998)
Авторское право © Sun Microsystems, Inc 1996 года. Все права защищены
Пожалуйста, отправьте любые комментарии или исправления к doug.kramer@sun.com