![]() |
Spec-Zone .ru
спецификации, руководства, описания, API
|
Содержание | Предыдущий | Следующий | Индекс | Спецификация языка Java Третий Выпуск |
ГЛАВА 17
В то время как большая часть обсуждения в предыдущих главах затрагивается только с поведением кода как выполняющийся единственный оператор или выражение за один раз, то есть, единственным потоком, каждая виртуальная машина Java может поддерживать много потоков выполнения сразу. Эти потоки независимо выполняют код, который работает на значениях и объектах, находящихся в совместно используемой основной памяти. Потоки могут поддерживаться при наличии многих аппаратных процессоров квантованием времени единственный аппаратный процессор, или квантованием времени много аппаратных процессоров.
Потоки представляются Thread
класс. Единственный способ для пользователя создать поток состоит в том, чтобы создать объект этого класса; каждый поток связывается с таким объектом. Поток запустится когда start()
метод вызывается на соответствие Thread
объект.
Поведение потоков, особенно если не правильно синхронизировалось, может сбить с толку и быть парадоксальным. Эта глава описывает семантику многопоточных программ; это включает правила, для которых значения могут быть замечены чтением совместно используемой памяти, которая обновляется многократными потоками. Поскольку спецификация подобна моделям памяти для различной аппаратной архитектуры, они, семантика известна как модель памяти языка программирования Java. Когда никакой беспорядок не сможет возникнуть, мы будем просто именовать эти правила как "модель памяти".
Они, которые не предписывает семантика, как должна быть выполнена многопоточная программа. Скорее они описывают поведения, которые многопоточным программам позволяют показать. Любая стратегия выполнения, которая генерирует только позволенный поведения, является приемлемой стратегией выполнения.
synchronized
оператор (§14.19) вычисляет ссылку на объект; это тогда пытается выполнить действие блокировки на мониторе того объекта и не продолжается далее, пока действие блокировки успешно не завершилось. После того, как действие блокировки было выполнено, тело synchronized
оператор выполняется. Если выполнение тела когда-либо завершается, или обычно или резко, разблокировать действие автоматически выполняется на том же самом мониторе.
A synchronized
метод (§8.4.3.6) автоматически выполняет действие блокировки, когда он вызывается; его тело не выполняется, пока действие блокировки успешно не завершилось. Если метод является методом экземпляра, он блокирует монитор, связанный с экземпляром, для которого он был вызван (то есть, объект, который будет известен как this
во время выполнения тела метода). Если метод статичен, он блокирует монитор, связанный с Class
объект, который представляет класс, в котором определяется метод. Если выполнение тела метода когда-либо завершается, или обычно или резко, разблокировать действие автоматически выполняется на том же самом мониторе.
Язык программирования Java ни не предотвращает, ни требует обнаружения условий мертвой блокировки. Программы, где потоки содержат (прямо или косвенно), соединяются, многократные объекты должны использовать стандартные методы для предотвращения мертвой блокировки, создавая высокоуровневые примитивы блокировки, которые не делают мертвой блокировки в случае необходимости.
Другие механизмы, такие как чтения и записи энергозависимых переменных и классов обеспечили в java.util.concurrent
пакет, обеспечьте альтернативные способы синхронизации.
r1
или r2
указать на переменные, локальные для метода или потока. Такие переменные не доступны другими потоками.Ограничения частичных порядков и функций. Мы используем f |d, чтобы обозначить функцию, данную, ограничивая домен f к d: для всего x в d, f |d (x) = f (x) и для всего x не в d, f |d (x) неопределено. Точно так же мы используем p|d, чтобы представить ограничение частичного порядка p к элементам в d: для всего x, y в d, p (x, y), если и только если p|d (x, y) = p (x). Если или x или y не находятся в d, то не то, что p|d (x, y).
Трассировка 17.1: Удивление результатов, вызванных переупорядочением оператора - исходный код
Поток 1
| Поток 2
|
---|---|
1: r2 = A;
| 3: r1 = B;
|
2: B = 1;
| 4: = 2;
|
Трассировка 17.2: Удивление результатов, вызванных переупорядочением оператора - допустимое преобразование компилятора
Поток 1
| Поток 2
|
---|---|
B = 1;
| r1 = B;
|
r2 = A;
| A = 2;
|
Считайте, например, пример показанным в Трассировке 17.1. Эта программа использует локальные переменные r1
и r2
и совместно используемые переменные A
и B
. Первоначально, A == B == 0.
Может казаться что результат r2 == 2, r1 == 1
невозможно. Интуитивно, или инструкция 1 или инструкция 3 должны быть на первом месте в выполнении. Если инструкция 1 на первом месте, она не должна быть в состоянии видеть запись в инструкции 4. Если инструкция 3 на первом месте, она не должна быть в состоянии видеть запись в инструкции 2.
Если бы некоторое выполнение, показанное это поведение, то мы знали бы, что инструкция 4 прибыла перед инструкцией 1, которая прибыла перед инструкцией 2, которая прибыла перед инструкцией 3, которая прибыла перед инструкцией 4. Это, на первый взгляд, абсурдно.
Однако, компиляторам позволяют переупорядочить инструкции в любом потоке, когда это не влияет на выполнение того потока в изоляции. Если инструкция 1 переупорядочивается с инструкцией 2, как показано в Трассировке 17.2, то легко видеть как результат r2 == 2
и r1 == 1
мог бы произойти.
Некоторым программистам это поведение может казаться "поврежденным". Однако, нужно отметить, что этот код ненадлежащим образом синхронизируется:
Несколько механизмов могут произвести переупорядочение в Трассировке 17.2. Своевременный компилятор и процессор могут перестроить код. Кроме того, иерархия памяти архитектуры, на которой выполняется виртуальная машина, может заставить ее появиться, как будто код переупорядочивается. В этой главе мы обратимся к чему-либо, что может переупорядочить код как компилятор.
Трассировка 17.3: Удивление результатов вызывается прямой заменой
Поток 1
| Поток 2
|
---|---|
r1 = p;
| r6 = p;
|
r2 = r1.x;
| r6.x = 3;
|
r3 = q;
|
|
r4 = r3.x;
|
|
r5 = r1.x;
|
|
|
|
Трассировка 17.4: Удивление результатов вызывается прямой заменой
Поток 1
| Поток 2
|
---|---|
r1 = p;
| r6 = p;
|
r2 = r1.x;
| r6.x = 3;
|
r3 = q;
|
|
r4 = r3.x;
|
|
r5 = r2;
|
|
|
|
Другой пример удивления результатов может быть замечен в Трассировке 17.3. Первоначально: p == q
, p.x == 0.
Эта программа также неправильно синхронизируется; это пишет в совместно используемую память, не осуществляя упорядочивания между теми записями.
Одна общая компиляторная оптимизация включает чтение значения для r2
снова использованный для r5
: они - оба чтения r1.x
без прошедшей записи. Эту ситуацию показывают в Трассировке 17.4.
Теперь рассмотрите случай где присвоение на r6.x
в Потоке 2 происходит между первым чтением r1.x
и чтение r3.x
в Потоке 1. Если компилятор решает снова использовать значение r2
для r5
, тогда r2
и r5
будет иметь значение 0, и r4
будет иметь значение 3. С точки зрения программиста, значение, сохраненное в p.x
изменился от 0 до 3 и затем возвратился.
Модель памяти описывает возможные поведения программы. Реализация свободна произвести любой код, который она любит, пока все получающееся выполнение программы приводит к результату, который может быть предсказан моделью памяти.
Обсуждение
Это обеспечивает большую свободу для конструктора выполнить несметное число преобразований кода, включая переупорядочение действий и удаления ненужной синхронизации.
Модель памяти определяет, какие значения могут быть считаны в каждой точке в программе. Действия каждого потока в изоляции должны вести себя как управляющийся семантикой того потока, за исключением того, что значения, замеченные каждым чтением, определяются моделью памяти. Когда мы обращаемся к этому, мы говорим, что программа повинуется семантике внутрипотока. Семантика внутрипотока является семантикой для единственных поточных программ, и позволяет полный прогноз поведения потока, основанного на значениях, замеченных действиями чтения в пределах потока. Чтобы определить, являются ли действия потока t в выполнении законными, мы просто оцениваем реализацию потока t, поскольку это было бы выполнено в единственном поточном контексте, как определено в остальной части этой спецификации.
Каждый раз, когда оценка потока t генерирует действие межпотока, это должно соответствовать действие межпотока t, который прибывает затем в порядок программы. Если чтения, то дальнейшая оценка t использует значение, замеченное как определено моделью памяти.
Этот раздел обеспечивает спецификацию модели памяти языка программирования Java за исключением проблем, имеющих дело с заключительными полями, которые описываются в §17.5.
Все поля экземпляра, статические поля и элементы массива сохранены в памяти "кучи". В этой главе мы используем термин переменная, чтобы обратиться к обоим полям и элементам массива. Локальные переменные (§14.4), формальные параметры метода (§8.4.1) или параметры обработчика исключений никогда не совместно используются потоками и незатронуты моделью памяти.
Два доступа к (чтения или записи к) та же самая переменная, как говорят, конфликтует, если по крайней мере один из доступов является записью.
Обсуждение
Действия расхождения потока представляются модели, как поток может заставить все другие потоки останавливаться и быть не в состоянии сделать успехи
Эта спецификация только касается действий межпотока. Мы не должны интересоваться действиями внутрипотока (например, добавляя две локальных переменные и храня результат в третьей локальной переменной). Как ранее упомянуто, все потоки должны повиноваться корректной семантике внутрипотока для программ Java. Мы обычно будем передруг, чтобы межраспараллелить действия более кратко как просто действия.
Действие описанного кортежем <t, k, v, u>, включая:
Параметры к внешнему действию (например, какие байты пишутся, к который сокет) не являются частью кортежа внешнего действия. Эти параметры устанавливаются другими действиями в пределах потока и могут быть определены, исследуя семантику внутрипотока. Они явно не обсуждаются в модели памяти.
В незавершающемся выполнении не все внешние действия заметны. Незавершение выполнения и заметных действий обсуждается в §17.4.9.
Ряд действий является последовательно непротиворечивым, если все действия происходят в полном порядке (порядок выполнения), который является непротиворечивым с порядком программы и кроме того, каждое чтение r переменной v видит значение, записанное записью w к v так, что:
Если у программы не будет никаких гонок данных, то все выполнение программы, будет казаться, будет последовательно непротиворечивым.
Последовательная непротиворечивость и/или свобода от гонок данных все еще позволяют ошибки, являющиеся результатом групп операций, которые должны быть восприняты атомарно и не являются.
Обсуждение
Если мы должны были использовать последовательную непротиворечивость в качестве нашей модели памяти, многие из компилятора и оптимизации процессора, которую мы обсудили, были бы недопустимы. Например, в Трассировке 17.3, как только запись 3 к p.x
произошедшие, последующие чтения того расположения были бы обязаны видеть то значение.
Действия синхронизации вызывают синхронизируемый - с отношением на действиях, определенных следующим образом:
false
или null
) к каждой переменной синхронизируется - с первым действием в каждом потоке. Хотя это может казаться немного странным, чтобы записать значение по умолчанию в переменную прежде, чем объект, содержащий переменную, будет выделен, концептуально каждый объект создается в начале программы с ее значением по умолчанию инициализированные значения.
T1.isAlive()
или T1.join()
.
InterruptedException
брошенный или вызывая Thread.interrupted
или Thread.isInterrupted
). Если у нас есть два действия x и y, мы пишем твердый черный (x, y), чтобы указать, что x происходит - прежде y.
Обсуждение
Например, запись значения по умолчанию к каждому полю объекта, созданного потоком, не должна произойти перед началом того потока, пока никакое чтение никогда не наблюдает тот факт.
Более определенно, если два действия совместно используют происхождение - перед отношением, они, должно не обязательно казаться, произошли в том порядке с любым кодом, с которым они не совместно используют происхождение - перед отношением. Записи в одном потоке, которые находятся в гонке данных с чтениями в другом потоке, может, например, казаться, происходят не в порядке с теми чтениями.
wait
методы класса Object
имейте блокировку и разблокируйте действия, связанные с ними; их происходит - прежде, чем отношения будут определены этими связанными действиями. Эти методы описываются далее в §17.8.
Происхождение - перед отношением определяет, когда гонки данных имеют место.
Ряд краев синхронизации, S, достаточен, если это - минимальный набор так, что, переходное закрытие S с порядком программы определяет все происхождение - перед краями в выполнении. Этот набор уникален.
Обсуждение
Это следует из вышеупомянутых определений что:
start()
на потоке происходит - перед любыми действиями в запущенном потоке.
join()
на том потоке.
Когда программа содержит два конфликтных доступа (§17.4.1), которые не упорядочиваются происхождением - перед отношением, она, как говорят, содержит гонку данных.
На семантику операций кроме действий межпотока, таких как чтения длин массива (§10.7), выполнение проверенных бросков (§5.5, §15.16), и вызовы виртуальных методов (§15.12), непосредственно не влияют гонки данных.
Обсуждение
Поэтому, гонка данных не может вызвать неправильное поведение, такое как возврат неправильной длины для массива.
Программа правильно синхронизируется, если и только если все последовательно непротиворечивое выполнение свободно от гонок данных.
Обсуждение
Тонкий пример неправильно синхронизируемого кода может быть замечен ниже. Данные показывают два различного выполнения той же самой программы, оба из которого содержит конфликтные доступы к совместно используемым переменным X
и Y
. Два потока в программе блокируют и разблокировали монитор M1
. В выполнении (a), есть происхождение - перед отношением между всеми парами конфликтных доступов. Однако, в выполнении (b), есть, не происходит - прежде, чем упорядочить между конфликтными доступами к X
. Из-за этого правильно не синхронизируется программа.
(a) Поток 1 получает блокировку сначала; Доступы к X упорядочиваются, происходит - прежде
(b) Поток 2 получает блокировку сначала; Доступы к X не упорядоченный происходят - прежде
Если программа будет правильно синхронизироваться, то все выполнение программы, будет казаться, будет последовательно непротиворечивым (§17.4.3).
Обсуждение
Это - чрезвычайно сильная гарантия программистов. Программисты не должны рассуждать о переупорядочиваниях, чтобы решить, что их код содержит гонки данных. Поэтому они не должны рассуждать о переупорядочиваниях, определяя, синхронизируется ли их код правильно. Как только определение, что код правильно синхронизируется, делается, программист не должен волноваться, что переупорядочивания будут влиять на его или её код.
Программа должна правильно синхронизироваться, чтобы избежать видов парадоксальных поведений, которые могут наблюдаться, когда код переупорядочивается. Использование корректной синхронизации не гарантирует, что полное поведение программы корректно. Однако, его использование действительно позволяет программисту рассуждать о возможных поведениях программы простым способом; поведение правильно синхронизируемой программы намного меньше зависит от возможных переупорядочиваний. Без корректной синхронизации очень странные, запутывающие и парадоксальные поведения возможны.
Мы говорим, что чтению r переменной v позволяют наблюдать запись w к v, если, в происхождении - прежде, чем частичный порядок выполнения прослеживает:
Трассировка 17.5: Поведение, позволенное, происходит - перед непротиворечивостью, но не последовательной непротиворечивостью. Может наблюдать r2 ==0
, r1 == 0
Поток 1
| Поток 2
|
---|---|
B = 1;
| A = 2;
|
r2 = A;
| r1 = B;
|
|
|
Ряд действий A, происходит - прежде непротиворечивый, если для всех чтений r в A, не то, что любой твердый черный (r, W (r)), где W (r) является действием записи, замеченным r или что там существует победа записи так, что w.v = r.v и твердый черный (W (r), w) и твердый черный (w, r).
Обсуждение
В происхождении - перед непротиворечивым множеством действий, каждое чтение видит запись, которую позволяется видеть происхождением - перед упорядочиванием.
Например, поведение, показанное в Трассировке 17.5, происходит - прежде непротиворечивый, так как есть порядки выполнения, которые позволяют каждому чтению видеть соответствующую запись.
Первоначально, A == B == 0
. В этом случае с тех пор нет никакой синхронизации, каждое чтение может видеть или запись начального значения или запись другим потоком. Один такой порядок выполнения
Точно так же поведение, показанное в Трассировке 17.5, происходит - прежде непротиворечивый, так как есть порядок выполнения, который позволяет каждому чтению видеть соответствующую запись. Порядок выполнения, который выводит на экран то поведение:1: B = 1; 3: A = 2; 2: r2 = A; // sees initial write of 0 4: r1 = B; // sees initial write of 0
В этом выполнении чтения видят записи, которые происходят позже в порядке выполнения. Это может казаться парадоксальным, но позволяется, происходит - перед непротиворечивостью. Разрешение чтений видеть более поздние записи может иногда производить недопустимые поведения.1: r2 = A; // sees write of A = 2 3: r1 = B; // sees write of B = 1 2: B = 1; 4: A = 2;
Выполнение, происходит - прежде непротиворечивый, если его набор действий, происходит - прежде непротиворечивый (§17.4.5).
Запускаясь с пустого множества как C0, мы выполняем последовательность шагов, где мы предпринимаем меры от набора действий A и добавляем их к ряду фиксировавших действий Ci, чтобы получить новый набор фиксировавших действий Ci+1. Демонстрировать, что это разумно для каждого Ci, мы должны демонстрировать выполнение Ei, содержащий Ci, который соблюдает определенные условия.
Формально, выполнение E удовлетворяет требования причинной связи модели памяти языка программирования Java, если и только если там существуют
Если A будет конечен, то последовательность C0, C1... будет конечна, заканчивающийся в наборе Cn = A. Однако, если A бесконечен, то последовательность, C0, C1... может быть бесконечным, и это должно иметь место, что объединение всех элементов этой бесконечной последовательности равно A.
Значения, записанные записями в Ci, должны быть тем же самым и в Ei и в E. Только чтения в Ci-1 должны видеть те же самые записи в Ei как в E. Формально,
Все чтения в Ei, которые не находятся в Ci-1, должны видеть записи, которые происходят - перед ними. Каждое чтение r в Ci - Ci-1 должен видеть записи в Ci-1 и в Ei и в E, но может видеть различную запись в Ei от того, который это видит в E. Формально,
Обсуждение
Происходит - Прежде, чем непротиворечивость будет необходимым, но не достаточная, набор ограничений. Просто осуществление происходит - прежде, чем непротиворечивость учла бы недопустимые поведения - те, которые нарушают требования, которые мы установили для программ. Например, происходит - прежде, чем непротиворечивость позволит значениям появляться "из ничего". Это может быть замечено подробным исследованием Трассировки 17.6.Трассировка 17.6: происходит - Прежде, чем непротиворечивость не будет достаточна
Поток 1
| Поток 2
|
---|---|
r1 = x;
| r2 = y;
|
if (r1 != 0) y = 1; | если (r2! = 0) x = 1;
|
|
|
Код, показанный в Трассировке 17.6, правильно синхронизируется. Это может казаться удивительным, так как это не выполняет действий синхронизации. Помните, однако, что программа правильно синхронизируется, если, когда она выполняется последовательно непротиворечивым способом, нет никаких гонок данных. Если этот код будет выполняться последовательно непротиворечивым способом, то каждое действие произойдет в порядке программы, и ни одна из записей не произойдет. Так как никакие записи не происходят, не может быть никаких гонок данных: программа правильно синхронизируется.
Так как эта программа правильно синхронизируется, единственные поведения, которые мы можем позволить, являются последовательно непротиворечивыми поведениями. Однако, есть выполнение этой программы, которая является, происходит - прежде непротиворечивый, но не последовательно непротиворечивый:
Этот результат, происходит - прежде непротиворечивый: есть, не происходит - перед отношением, которое препятствует тому, чтобы это произошло. Однако, это ясно не приемлемо: нет никакого последовательно непротиворечивого выполнения, которое привело бы к этому поведению. Факт, что мы позволяем чтению видеть запись, которая прибывает позже в порядок выполнения, может иногда таким образом приводить к недопустимым поведениям.r1 = x; // sees write of x = 1 y = 1; r2 = y; // sees write of y = 1 x = 1;
Хотя разрешение чтений видеть записи, которые прибывают позже в порядок выполнения, иногда является нежелательным, это также иногда необходимо. Как мы видели выше, Трассировка 17.5 требует, чтобы некоторые чтения видели записи, которые происходят позже в порядке выполнения. Так как чтения на первом месте в каждом потоке, самое первое действие в порядке выполнения должно быть чтением. Если то чтение не может видеть запись, которая происходит позже, то оно не может видеть значение кроме начального значения для переменной, которую оно читает. Это является ясно не отражающим из всех поведений.
Мы обращаемся к проблеме того, когда чтения могут рассмотреть будущие записи как причинную связь из-за проблем, которые возникают в случаях как тот, найденный в Трассировке 17.6. В этом случае чтения заставляют записи происходить, и записи заставляют чтения происходить. Есть никакая "первая причина" для действий. Наша модель памяти поэтому нуждается в непротиворечивом способе определить, какие чтения могут видеть записи рано.
Примеры, такие как тот, найденный в Трассировке 17.6, демонстрируют, что спецификация должна быть осторожной, утверждая, может ли чтение видеть запись, которая происходит позже в выполнении (принимающий во внимание, что, если чтение видит запись, которая происходит позже в выполнении, это представляет факт, что запись фактически выполняется рано).
Модель памяти берет как входной данное выполнение, и программу, и определяет, является ли то выполнение юридическим выполнением программы. Это делает это, постепенно создавая ряд "фиксировавших" действий, которые отражаются, какие действия выполнялись программой. Обычно, следующее действие, которое будет фиксироваться, отразит следующее действие, которое может быть выполнено последовательно непротиворечивым выполнением. Однако, чтобы отразить чтения, которые должны видеть более поздние записи, мы позволяем некоторым действиям фиксироваться ранее чем другие действия, которые происходят - перед ними.
Очевидно, некоторые действия могут фиксироваться рано, и некоторые не могут. Если бы, например, одна из записей в Трассировке 17.6 фиксировалась то перед чтением той переменной чтение могло видеть запись, и "из ничего", результат мог произойти. Неофициально, мы позволяем действию фиксироваться рано, если мы знаем, что действие может произойти, не предполагая, что некоторая гонка данных происходит. В Трассировке 17.6, мы не можем выполнить ни одну запись рано, потому что записи не могут произойти, если чтения не видят результат гонки данных.
Заметное поведение программы определяется конечными множествами внешних действий, которые может выполнить программа. Программа, которая, например, просто печатает "Привет" навсегда, описывается рядом поведений, что для любого неотрицательного целого числа i, включает поведение печати "Привет" меня времена.
Завершение явно не моделируется как поведение, но программа может легко быть расширена, чтобы генерировать дополнительное внешнее действие executionTermination, который происходит, когда все потоки завершились.
Мы также определяем специальное предложение, подвешивают действие. Если поведение описывается рядом внешних действий включая подвешивать действие, это указывает на поведение, где после того, как внешние действия наблюдаются, программа может работать за неограниченным количеством времени, не выполняя дополнительных внешних действий или завершения. Программы могут зависнуть, если все потоки блокируются или если программа может выполнить неограниченное число действий, не выполняя внешних действий.
Поток может быть блокирован во множестве обстоятельств, такой как тогда, когда это пытается получить блокировку или выполнить внешнее действие (такое как чтение), который зависит от внешних данных. Если поток находится в таком состоянии, Thread.getState
возвратится BLOCKED
или WAITING
.
Выполнение может привести к потоку, блокируемому неопределенно и не завершение выполнения. В таких случаях действия, сгенерированные блокированным потоком, должны состоять из всех действий, сгенерированных тем потоком до и включая действие, которое заставило поток быть блокированным, и никакие действия, которые будут сгенерированы потоком после того действия.
Чтобы рассуждать о заметных поведениях, мы должны говорить о наборах заметных действий.
Если O является рядом заметных действий foran выполнение E, то установленный O должен быть подмножеством действий Э, A, и должен содержать только конечное число действий, даже если A содержит бесконечное число действий. Кроме того, если действие y находится в O, и любом твердом черном (x, y) или так (x, y), то x находится в O.
Отметьте, что ряд заметных действий не ограничивается внешним действиям. Скорее только внешние действия, которые находятся в ряде заметных действий, как считают, являются заметными внешними действиями.
Поведение B является допустимым поведением программы P, если и только если B является конечным множеством внешних действий и также
Обсуждение
Отметьте, что поведение B не описывает порядок, в котором внешние действия в B наблюдаются, но другие (внутренние) ограничения на то, как внешние действия сгенерированы и выполнены, может наложить такие ограничения.
final
инициализируются однажды, но никогда не изменяются при нормальных обстоятельствах. Подробная семантика заключительных полей несколько отличается от таковых из нормальных полей. В частности у компиляторов есть большая свобода переместить чтения заключительных полей через барьеры синхронизации и звонки в произвольные или неизвестные методы. Соответственно, компиляторам позволяют сохранить значение заключительного поля кэшируемым в регистре и не перезагрузить его из памяти в ситуациях, где незаключительное поле должно было бы быть перезагружено.Заключительные поля также позволяют программистам реализовывать ориентированные на многопотоковое исполнение неизменные объекты без синхронизации. Ориентированный на многопотоковое исполнение неизменный объект замечается как неизменный всеми потоками, даже если гонка данных используется, чтобы передать ссылки на неизменный объект между потоками. Это может обеспечить гарантии безопасности от неправильного употребления неизменного класса неправильным или вредоносным кодом. Заключительные поля должны использоваться правильно, чтобы обеспечить гарантию неизменности.
Объект, как полагают, полностью инициализируется, когда его конструктор заканчивает. Поток, который может только видеть ссылку на объект после того объекта, был полностью инициализирован, как, гарантируют, будет видеть правильно инициализированные значения для заключительных полей того объекта.
Модель использования для заключительных полей является простой. Установите заключительные поля для объекта в конструкторе того объекта. Не пишите ссылку на объект, создаваемый в месте, где другой поток может видеть это прежде, чем конструктор объекта будет закончен. Если это будет сопровождаться, то, когда объект замечается другим потоком, тот поток будет всегда видеть правильно созданную версию заключительных полей того объекта. Это будет также видеть версии любого объекта или массива, на который ссылаются те заключительные поля, которые, по крайней мере, столь же актуальны, как заключительные поля.
Обсуждение
Пример ниже иллюстрирует, как заключительные поля сравниваются с нормальными полями.
Классclass FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null) { int i = f.x; // guaranteed to see 3 int j = f.y; // could see 0 } } }
FinalFieldExample
имеет заключительное международное поле x
и незаключительное международное поле y
. Один поток мог бы выполнить метод writer(),
и другой мог бы выполнить метод reader().
Поскольку writer()
записи f
после того, как конструктор объекта заканчивает, reader()
как будут гарантировать, будет видеть должным образом инициализированное значение для f.x
: это считает значение 3. Однако, f.y
не является заключительным; reader()
метод, как поэтому гарантируют, не будет видеть значение 4 для этого.
Обсуждение
Заключительные поля разрабатываются, чтобы учесть необходимые гарантии безопасности. Рассмотрите следующий пример. Один поток (который мы будем именовать как поток 1) выполняется
в то время как другой поток (распараллеливают 2) выполняетсяGlobal.s = "/tmp/usr".substring(4);
String myS = Global.s; if (myS.equals("/tmp"))System.out.println(myS);
String
объекты предназначаются, чтобы быть неизменными, и строковые операции не выполняют синхронизацию. В то время как String
у реализации нет никаких гонок данных, у другого кода могли быть гонки данных, включающие использование String
s, и модель памяти делает слабые гарантии программ, у которых есть гонки данных. В частности если поля String
класс не был заключительным, тогда это будет возможно (хотя вряд ли), что Поток 2 мог первоначально видеть значение по умолчанию 0 для смещения строкового объекта, позволяя это сравниться как равный "/tmp"
. Более поздняя работа на String
объект мог бы видеть корректное смещение 4, так, чтобы String
объект воспринимается как являющийся "/usr"
. Много средств защиты языка программирования Java зависят от String
s воспринимаемый как действительно неизменный, даже если вредоносный код использует гонки данных, чтобы передать String
ссылки между потоками.
Обсуждение
Отметьте, что, если один конструктор вызывает другого конструктора, и вызванный конструктор устанавливает заключительное поле, замораживание для заключительного поля имеет место в конце вызванного конструктора.
Для каждого выполнения поведение чтений под влиянием двух дополнительных частичных порядков, разыменовать цепочка разыменовывает () и мегагерц цепочки памяти (), которые, как полагают, являются частью выполнения (и таким образом, фиксируются для любого определенного выполнения). Эти частичные порядки должны удовлетворить следующие ограничения (у которого не должно быть уникального решения):
Для чтений заключительных полей единственные записи, которые, как считают, прибывают перед чтением заключительного поля, являются теми полученными через заключительную полевую семантику.
Даже тогда есть много сложностей. Если заключительное поле инициализируется ко времени компиляции, постоянному в полевом объявлении, изменения к заключительному полю не могут наблюдаться, так как использование того заключительного поля заменяется во время компиляции с постоянным временем компиляции.
Другая проблема состоит в том, что спецификация позволяет агрессивную оптимизацию заключительных полей. В пределах потока допустимо переупорядочить чтения заключительного поля с теми модификациями заключительного поля, которые не имеют место в конструкторе.
Обсуждение
Например, рассмотрите следующий фрагмент кода:
Вclass A { final int x; A() { x = 1; } int f() { return d(this,this); } int d(A a1, A a2) { int i = a1.x; g(a1); int j = a2.x; return j - i; } static void g(A a) { // uses reflection to change a.x to 2 } }
d()
метод, компилятору позволяют переупорядочить чтения x
и звонок g()
свободно. Таким образом, A().f()
мог возвратиться-1, 0 или 1.
Реализация может обеспечить способ выполнить блок кода в заключительном поле безопасный контекст. Если объект будет создан в заключительном поле безопасный контекст, то чтения заключительного поля того объекта не будут переупорядочены с модификациями того заключительного поля, которые происходят в том заключительном поле безопасный контекст.
У заключительного поля безопасный контекст есть дополнительные защиты. Если поток видел неправильно опубликованную ссылку на объект, который позволяет потоку видеть, что значение по умолчанию заключительного поля, и затем, в пределах заключительно-полевого безопасного контекста, читает должным образом опубликованную ссылку на объект, это, как будут гарантировать, будет видеть корректное значение заключительного поля. В формализме код, выполняемый в пределах заключительно-полевого безопасного контекста, обрабатывается как отдельный поток (в целях заключительной полевой семантики только).
В реализации компилятор не должен переместить доступ к заключительному полю в или из заключительно-полевого безопасного контекста (хотя он может быть перемещен вокруг выполнения такого контекста, пока объект не создается в пределах того контекста).
Одно место, где использование заключительно-полевого безопасного контекста было бы соответствующим, находится в исполнителе или пуле потоков. Выполняя каждого Runnable
в отдельном заключительном поле безопасный контекст исполнитель мог гарантировать то некорректное обращение одним Runnable
к объекту o не будет удалять заключительные полевые гарантии другого Runnable
s обработанный тем же самым исполнителем.
System.in
, System.out
, и System.err
заключительные статические поля, которым, по причинам наследства, нужно позволить быть измененными методами System.setIn
, System.setOut
и System.setErr
. Мы именуем эти поля, как являющиеся защищенным от записи, чтобы отличить их от обычных заключительных полей.
Компилятор должен обработать эти поля по-другому от других заключительных полей. Например, чтение обычного заключительного поля "неуязвимо" к синхронизации: барьер, включенный в блокировку или энергозависимое чтение, не должен влиять на то, какое значение читается из заключительного поля. Так как значение полей защищенных от записи, как может замечаться, изменяется, события синхронизации должны иметь эффект на них. Поэтому, семантика диктуют, что эти поля обрабатываются как нормальные поля, которые не могут быть изменены пользовательским кодом, если тот пользовательский код не находится в System
класс.
Некоторые процессоры не обеспечивают возможность записать в единственный байт. Это было бы недопустимо, чтобы реализовать обновления байтового массива о таком процессоре, просто читая все слово, обновляя соответствующий байт, и затем записывая все слово обратно к памяти. Эта проблема иногда известна как разрывание слова, и на процессорах, которые не могут легко обновить единственный байт в изоляции, некоторый другой подход будет требоваться.
Обсуждение
Вот прецедент, чтобы обнаружить разрывание слова:
Это делает точку, что байты не должны быть перезаписаны записями к смежным байтамpublic class WordTearing extends Thread { static final int LENGTH = 8; static final int ITERS = 1000000; static byte[] counts = new byte[LENGTH]; static Thread[] threads = new Thread[LENGTH]; final int id; WordTearing(int i) { id = i; } public void run() { byte v = 0; for (int i = 0; i < ITERS; i++) { byte v2 = counts[id]; if (v != v2) { System.err.println("Word-Tearing found: " + "counts[" + id + "] = " + v2 + ", should be " + v); return; } v++; counts[id] = v; } } public static void main(String[] args) { for (int i = 0; i < LENGTH; ++i) (threads[i] = new WordTearing(i)).start(); } }
В целях модели памяти языка программирования Java единственная запись к энергонезависимому длинному или двойному значению обрабатывается как две отдельных записи: один к каждой 32-разрядной половине. Это может привести к ситуации, где поток видит первые 32 бита 64 битовых значений от одной записи, и вторые 32 бита от другой записи. Записи и чтения энергозависимых длинных и двойных значений являются всегда атомарными. Записи к и чтения ссылок являются всегда атомарными, независимо от того, реализуются ли они как 32 или 64 битовых значения.
Конструкторы VM поощряются избежать разделять их 64-разрядные значения где только возможно. Программисты поощряются объявить совместно использованные 64-разрядные значения как энергозависимые или синхронизировать их программы правильно, чтобы избежать возможных сложностей.
wait()
, или синхронизированные формы wait(long millisecs)
и wait(long millisecs, int nanosecs)
. Вызов wait(long millisecs)
с параметром нуля, или вызовом wait(long millisecs, int nanosecs)
с двумя нулевыми параметрами, эквивалентно вызову wait()
.
Поток обычно возвращается из a wait
если это возвращается, не бросая InterruptedException.
Позвольте потоку t быть потоком, выполняющим ожидать метод на объектном м., и позволять n быть числом действий блокировки t на м., которые не были соответствующими, разблокировали действия. Одно из следующих действий происходит.
IllegalMonitorStateException
бросается.
IllegalArgumentException
бросается.
InterruptedException
бросается и состояние прерывания t устанавливается в ложь.
millisecs
миллисекунды плюс nanosecs
наносекунды протекают с начала этого ожидают действие.
wait
только в пределах циклов, которые завершаются только, когда некоторое логическое условие, что поток ожидает хранений. Каждый поток должен определить порядок по событиям, которые могли заставить его быть удаленным из ожидать набора. Тот порядок не должен быть непротиворечивым с другими упорядочиваниями, но поток должен вести себя, как если бы те события имели место в том порядке.
Например, если поток t находится в ожидать наборе для м., и затем и прерывание t и уведомление о м. происходят, должен быть порядок по этим событиям.
Если прерывание, как будут считать, произошло сначала, то t в конечном счете возвратится из wait
бросая InterruptedException
, и некоторый другой поток в ожидать наборе для м. (если кто-либо существует во время уведомления) должен получить уведомление. Если уведомление, как будут считать, произошло сначала, то t будет в конечном счете обычно возвращаться из wait
с прерыванием, все еще ожидающим.
InterruptedException
. notify
и notifyAll
. Позвольте потоку t быть потоком, выполняющим любой из этих методов на объектном м., и позволять n быть числом действий блокировки t на м., которые не были соответствующими, разблокировали действия. Одно из следующих действий происходит.
IllegalMonitorStateException
бросается. Дело обстоит так, где поток t уже не обладает блокировкой для целевого м.
notify
действие, тогда, если м. ожидает набор, не пусто, поток u, который является элементом тока м., ожидают, набор выбирается и удаляется из ожидать набора. (Нет никакой гарантии, о которой выбирается поток в ожидать наборе.) Это удаление от ожидать набора включает возобновлению u в ожидать действии. Заметьте однако, за которым не могут следовать действия блокировки u после возобновления, пока некоторое время после t полностью не разблокировало монитор для м.
notifyAll
действие, тогда все потоки удаляются из м., ожидают набор, и таким образом возобновляются. Заметьте однако, это, только один из них за один раз заблокирует монитор, требуемый во время возобновления wait.
Thread.interrupt
, так же как методы, определенные, чтобы вызвать это поочередно, такой как ThreadGroup.interrupt
. Позвольте t быть вызовом u потока.interrupt
, для некоторого потока u, где t и u могут быть тем же самым. Это действие заставляет состояние прерывания u быть установленным в истину.
Дополнительно, если там существует некоторый объектный м., чей ожидают, набор содержит u, u удаляется из м., ожидают набор. Это позволяет u возобновиться в ожидать действии, когда это ожидает после переблокировки монитора м., бросит InterruptedException
.
Вызовы Thread.isInterrupted
может определить состояние прерывания потока. Статический метод Thread.interrupted
может быть вызван потоком, чтобы наблюдать и очистить его собственное состояние прерывания.
wait
, все еще имея прерывание на ожидании (в других работах, звонке Thread.interrupted
возвратил бы true),
wait
бросая InterruptedException
wait
.
Точно так же уведомления не могут быть потеряны из-за прерываний. Предположите, что набор s потоков находится в ожидать наборе объектного м., и другой поток выполняет a notify
на м. Затем также
wait
, или
wait
бросая InterruptedException
notify
, и тот поток возвращается из wait
бросая InterruptedException
, тогда некоторый другой поток в ожидать наборе должен быть уведомлен.Thread.sleep
заставляет в настоящий момент выполняющийся поток спать (временно прекращают выполнение) для указанной продолжительности согласно точности и точности системных таймеров и планировщиков. Поток не теряет владение любых мониторов, и возобновление выполнения будет зависеть от планирования и доступности процессоров, на которых можно выполнить поток.Ни сон сроком на нулевое время, ни работа урожая не должны иметь заметные эффекты.
Важно не отметить это ни один Thread.sleep
ни Thread.yield
имейте любую семантику синхронизации. В частности компилятор не должен сбросить записи, кэшируемые в регистрах к совместно используемой памяти перед звонком Thread.sleep
или Thread.yield
, и при этом компилятор не должен перезагрузить значения, кэшируемые в регистрах после звонка Thread.sleep
или Thread.yield.
Обсуждение
Например, в следующем (поврежденном) фрагменте кода, примите это this.done
энергонезависимое булево поле:
Компилятор свободен считать полеwhile (!this.done) Thread.sleep(1000);
this.done
только однажды, и повторное использование кэшируемое значение в каждом выполнении цикла. Это означало бы, что цикл никогда не будет завершаться, даже если другой поток, измененный значение this.done
.
Содержание | Предыдущий | Следующий | Индекс | Спецификация языка Java Третий Выпуск |
Авторское право © 1996-2005 Sun Microsystems, Inc. Все права защищены
Пожалуйста, отправьте любые комментарии или исправления через нашу