Spec-Zone .ru
спецификации, руководства, описания, API
Содержание | Предыдущий | Следующий | Индекс Спецификация языка Java
Третий Выпуск


ГЛАВА 17

Потоки и Блокировки


В то время как большая часть обсуждения в предыдущих главах затрагивается только с поведением кода как выполняющийся единственный оператор или выражение за один раз, то есть, единственным потоком, каждая виртуальная машина Java может поддерживать много потоков выполнения сразу. Эти потоки независимо выполняют код, который работает на значениях и объектах, находящихся в совместно используемой основной памяти. Потоки могут поддерживаться при наличии многих аппаратных процессоров квантованием времени единственный аппаратный процессор, или квантованием времени много аппаратных процессоров.

Потоки представляются Thread класс. Единственный способ для пользователя создать поток состоит в том, чтобы создать объект этого класса; каждый поток связывается с таким объектом. Поток запустится когда start() метод вызывается на соответствие Thread объект.

Поведение потоков, особенно если не правильно синхронизировалось, может сбить с толку и быть парадоксальным. Эта глава описывает семантику многопоточных программ; это включает правила, для которых значения могут быть замечены чтением совместно используемой памяти, которая обновляется многократными потоками. Поскольку спецификация подобна моделям памяти для различной аппаратной архитектуры, они, семантика известна как модель памяти языка программирования Java. Когда никакой беспорядок не сможет возникнуть, мы будем просто именовать эти правила как "модель памяти".

Они, которые не предписывает семантика, как должна быть выполнена многопоточная программа. Скорее они описывают поведения, которые многопоточным программам позволяют показать. Любая стратегия выполнения, которая генерирует только позволенный поведения, является приемлемой стратегией выполнения.

17.1 Блокировки

Язык программирования Java обеспечивает многократные механизмы для того, чтобы они связались между потоками. Самым основным из этих методов является синхронизация, которая реализуется, используя мониторы. Каждый объект в Java связывается с монитором, который поток может заблокировать или разблокировать. Только один поток за один раз может содержать блокировку на мониторе. Любые другие потоки, пытающиеся заблокировать тот монитор, блокируются, пока они не могут получить блокировку на том мониторе. Поток t может заблокировать определенный монитор многократно; каждый разблокировал реверсы эффект одной работы блокировки.

synchronized оператор (§14.19) вычисляет ссылку на объект; это тогда пытается выполнить действие блокировки на мониторе того объекта и не продолжается далее, пока действие блокировки успешно не завершилось. После того, как действие блокировки было выполнено, тело synchronized оператор выполняется. Если выполнение тела когда-либо завершается, или обычно или резко, разблокировать действие автоматически выполняется на том же самом мониторе.

A synchronized метод (§8.4.3.6) автоматически выполняет действие блокировки, когда он вызывается; его тело не выполняется, пока действие блокировки успешно не завершилось. Если метод является методом экземпляра, он блокирует монитор, связанный с экземпляром, для которого он был вызван (то есть, объект, который будет известен как this во время выполнения тела метода). Если метод статичен, он блокирует монитор, связанный с Class объект, который представляет класс, в котором определяется метод. Если выполнение тела метода когда-либо завершается, или обычно или резко, разблокировать действие автоматически выполняется на том же самом мониторе.

Язык программирования Java ни не предотвращает, ни требует обнаружения условий мертвой блокировки. Программы, где потоки содержат (прямо или косвенно), соединяются, многократные объекты должны использовать стандартные методы для предотвращения мертвой блокировки, создавая высокоуровневые примитивы блокировки, которые не делают мертвой блокировки в случае необходимости.

Другие механизмы, такие как чтения и записи энергозависимых переменных и классов обеспечили в java.util.concurrent пакет, обеспечьте альтернативные способы синхронизации.

17.2 Нотация в Примерах

Модель памяти, определенная здесь, существенно не базируется в объектно-ориентированной природе языка программирования Java. Для осмысленности и простоты в наших примерах, мы часто показываем фрагменты кода без класса или определений метода, или явного разыменования. Большинство примеров состоит из двух или больше потоков, содержащих операторы с доступом к локальным переменным, совместно использованным глобальным переменным или полям экземпляра объекта. Мы обычно используем имена переменных такой как 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.3 Неправильно Синхронизируемые Поведения Удивления приложения Программ

Семантика языка программирования Java позволяет компиляторам и микропроцессорам выполнять оптимизацию, которая может взаимодействовать с неправильно синхронизируемым кодом способами, которые могут произвести поведения, которые кажутся парадоксальными.

Трассировка 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.4.5). Когда код содержит гонку данных, парадоксальные результаты часто возможны.

Несколько механизмов могут произвести переупорядочение в Трассировке 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 и затем возвратился.

17.4 Модель памяти

Модель памяти описывает учитывая программу и трассировку выполнения той программы, является ли трассировка выполнения юридическим выполнением программы. Модель памяти языка программирования Java работает, исследуя каждое чтение в трассировке выполнения и проверяя, что запись, наблюдаемая тем чтением, допустима согласно определенным правилам.

Модель памяти описывает возможные поведения программы. Реализация свободна произвести любой код, который она любит, пока все получающееся выполнение программы приводит к результату, который может быть предсказан моделью памяти.


Обсуждение

Это обеспечивает большую свободу для конструктора выполнить несметное число преобразований кода, включая переупорядочение действий и удаления ненужной синхронизации.


Модель памяти определяет, какие значения могут быть считаны в каждой точке в программе. Действия каждого потока в изоляции должны вести себя как управляющийся семантикой того потока, за исключением того, что значения, замеченные каждым чтением, определяются моделью памяти. Когда мы обращаемся к этому, мы говорим, что программа повинуется семантике внутрипотока. Семантика внутрипотока является семантикой для единственных поточных программ, и позволяет полный прогноз поведения потока, основанного на значениях, замеченных действиями чтения в пределах потока. Чтобы определить, являются ли действия потока t в выполнении законными, мы просто оцениваем реализацию потока t, поскольку это было бы выполнено в единственном поточном контексте, как определено в остальной части этой спецификации.

Каждый раз, когда оценка потока t генерирует действие межпотока, это должно соответствовать действие межпотока t, который прибывает затем в порядок программы. Если чтения, то дальнейшая оценка t использует значение, замеченное как определено моделью памяти.

Этот раздел обеспечивает спецификацию модели памяти языка программирования Java за исключением проблем, имеющих дело с заключительными полями, которые описываются в §17.5.

17.4.1 Совместно используемые переменные

Память, которая может быть совместно использована потоками, вызывают память "кучи" или совместно используемая память.

Все поля экземпляра, статические поля и элементы массива сохранены в памяти "кучи". В этой главе мы используем термин переменная, чтобы обратиться к обоим полям и элементам массива. Локальные переменные (§14.4), формальные параметры метода (§8.4.1) или параметры обработчика исключений никогда не совместно используются потоками и незатронуты моделью памяти.

Два доступа к (чтения или записи к) та же самая переменная, как говорят, конфликтует, если по крайней мере один из доступов является записью.

17.4.2 Действия

Действие межпотока является действием, выполняемым одним потоком, который может быть обнаружен или непосредственно под влиянием другого потока. Есть несколько видов действия межпотока, которое может выполнить программа:


Обсуждение

Действия расхождения потока представляются модели, как поток может заставить все другие потоки останавливаться и быть не в состоянии сделать успехи


Эта спецификация только касается действий межпотока. Мы не должны интересоваться действиями внутрипотока (например, добавляя две локальных переменные и храня результат в третьей локальной переменной). Как ранее упомянуто, все потоки должны повиноваться корректной семантике внутрипотока для программ Java. Мы обычно будем передруг, чтобы межраспараллелить действия более кратко как просто действия.

Действие описанного кортежем <t, k, v, u>, включая:

Кортеж внешнего действия содержит дополнительный компонент, который содержит результаты внешнего действия как воспринято потоком, выполняющим действие. Это может быть информацией относительно успеха или провала действия, и любых значений, считанных действием.

Параметры к внешнему действию (например, какие байты пишутся, к который сокет) не являются частью кортежа внешнего действия. Эти параметры устанавливаются другими действиями в пределах потока и могут быть определены, исследуя семантику внутрипотока. Они явно не обсуждаются в модели памяти.

В незавершающемся выполнении не все внешние действия заметны. Незавершение выполнения и заметных действий обсуждается в §17.4.9.

17.4.3 Программы и Порядок Программы

Среди всех действий межпотока, выполняемых каждым потоком t, порядок программы t является полным порядком, который отражает порядок, в котором эти действия были бы выполнены согласно семантике внутрипотока t.

Ряд действий является последовательно непротиворечивым, если все действия происходят в полном порядке (порядок выполнения), который является непротиворечивым с порядком программы и кроме того, каждое чтение r переменной v видит значение, записанное записью w к v так, что:

Последовательная непротиворечивость является очень сильной гарантией, которая делается о видимости и упорядочивающий в выполнении программы. В пределах последовательно непротиворечивого выполнения есть полный порядок по всем отдельным действиям (таким как чтения и записи), который является непротиворечивым с порядком программы, и каждое отдельное действие является атомарным и является сразу видимым к каждому потоку.

Если у программы не будет никаких гонок данных, то все выполнение программы, будет казаться, будет последовательно непротиворечивым.

Последовательная непротиворечивость и/или свобода от гонок данных все еще позволяют ошибки, являющиеся результатом групп операций, которые должны быть восприняты атомарно и не являются.


Обсуждение

Если мы должны были использовать последовательную непротиворечивость в качестве нашей модели памяти, многие из компилятора и оптимизации процессора, которую мы обсудили, были бы недопустимы. Например, в Трассировке 17.3, как только запись 3 к p.x произошедшие, последующие чтения того расположения были бы обязаны видеть то значение.


17.4.4 Порядок синхронизации

У каждого выполнения есть порядок синхронизации. Порядок синхронизации является полным порядком по всем действиям синхронизации выполнения. Для каждого потока t, порядок синхронизации действий синхронизации (§17.4.2) в t является непротиворечивым с порядком программы (§17.4.3) t.

Действия синхронизации вызывают синхронизируемый - с отношением на действиях, определенных следующим образом:

Источник синхронизирования - с краем вызывают выпуском, и место назначения вызывают получением.

17.4.5 Происходит - перед Порядком

Два действия могут быть упорядочены происхождением - перед отношением. Если одно действие происходит - перед другим, то первое видимо к и упорядоченный перед вторым.

Если у нас есть два действия x и y, мы пишем твердый черный (x, y), чтобы указать, что x происходит - прежде y.

Нужно отметить, что присутствие происхождения - перед отношением между двумя действиями не обязательно подразумевает, что они должны иметь место в том порядке в реализации. Если переупорядочение приводит к результатам, непротиворечивым с юридическим выполнением, это не недопустимо.


Обсуждение

Например, запись значения по умолчанию к каждому полю объекта, созданного потоком, не должна произойти перед началом того потока, пока никакое чтение никогда не наблюдает тот факт.

Более определенно, если два действия совместно используют происхождение - перед отношением, они, должно не обязательно казаться, произошли в том порядке с любым кодом, с которым они не совместно используют происхождение - перед отношением. Записи в одном потоке, которые находятся в гонке данных с чтениями в другом потоке, может, например, казаться, происходят не в порядке с теми чтениями.


wait методы класса Object имейте блокировку и разблокируйте действия, связанные с ними; их происходит - прежде, чем отношения будут определены этими связанными действиями. Эти методы описываются далее в §17.8.

Происхождение - перед отношением определяет, когда гонки данных имеют место.

Ряд краев синхронизации, S, достаточен, если это - минимальный набор так, что, переходное закрытие S с порядком программы определяет все происхождение - перед краями в выполнении. Этот набор уникален.


Обсуждение

Это следует из вышеупомянутых определений что:


Когда программа содержит два конфликтных доступа (§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, если, в происхождении - прежде, чем частичный порядок выполнения прослеживает:

Неофициально, чтению r позволяют видеть результат записи w, если есть, не происходит - прежде, чем упорядочить, чтобы предотвратить то чтение.

Трассировка 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. В этом случае с тех пор нет никакой синхронизации, каждое чтение может видеть или запись начального значения или запись другим потоком. Один такой порядок выполнения

1: B = 1;
3: A = 2;
2: r2 = A; // sees initial write of 0
4: r1 = B; // sees initial write of 0
Точно так же поведение, показанное в Трассировке 17.5, происходит - прежде непротиворечивый, так как есть порядок выполнения, который позволяет каждому чтению видеть соответствующую запись. Порядок выполнения, который выводит на экран то поведение:

1: r2 = A; // sees write of A = 2
3: r1 = B; // sees write of B = 1
2: B = 1;
4: A = 2;
В этом выполнении чтения видят записи, которые происходят позже в порядке выполнения. Это может казаться парадоксальным, но позволяется, происходит - перед непротиворечивостью. Разрешение чтений видеть более поздние записи может иногда производить недопустимые поведения.


17.4.6 Выполнение

Выполнение E описывается кортежем <P, A, почтовый, таким образом, W, V, коротковолновый, твердый черный>, включая:

Отметьте, что синхронизирование - с и происходит - прежде, чем будут уникально определены другими компонентами выполнения и правил для правильно построенного выполнения (§17.4.7).

Выполнение, происходит - прежде непротиворечивый, если его набор действий, происходит - прежде непротиворечивый (§17.4.5).

17.4.7 Правильно построенное Выполнение

Мы только рассматриваем правильно построенное выполнение. Выполнение E = <P, A, почтовый, таким образом, W, V, коротковолновый, твердый черный> хорошо формируется, если следующие условия являются истиной:

  1. Каждое чтение видит запись к той же самой переменной в выполнении. Все чтения и записи энергозависимых переменных являются энергозависимыми действиями. Для всех чтений r в A, у нас есть W (r) в A и W (r).v = r.v. Переменная r.v энергозависима, если и только если r является энергозависимым чтением, и переменная w.v энергозависима, если и только если w является энергозависимой записью.
  2. Происходит - прежде, чем порядок будет частичным порядком. Происходит - прежде, чем порядок будет дан переходным закрытием, синхронизируется - с порядком программы и краями. Это должен быть допустимый частичный порядок: рефлексивный, переходный и антисимметричный.
  3. Выполнение повинуется непротиворечивости внутрипотока. Для каждого потока t, действия, выполняемые t в A, являются тем же самым, как был бы сгенерирован тем потоком в порядке программы в изоляции, с каждой записью wwriting значение V (w), учитывая что каждое чтение r видит значение V (W (r)). Значения, замеченные каждым чтением, определяются моделью памяти. Данный порядок программы должен отразить порядок программы, в котором действия были бы выполнены согласно семантике внутрипотока P.
  4. Выполнение, происходит - прежде непротиворечивый (§17.4.6).
  5. Выполнение повинуется непротиворечивости порядка синхронизации. Для всех энергозависимых чтений r в A, не то, что любой так (r, W (r)) или что там существует победа записи так, что w.v = r.v и так (W (r), w) и так (w, r).

17.4.8 Выполнение и Требования Причинной связи

Правильно построенное выполнение E = <P, A, почтовый, таким образом, W, V, коротковолновый, твердый черный> проверяется, фиксируя действия от A. Если все действия в A могут фиксироваться, то выполнение удовлетворяет требования причинной связи модели памяти языка программирования Java.

Запускаясь с пустого множества как C0, мы выполняем последовательность шагов, где мы предпринимаем меры от набора действий A и добавляем их к ряду фиксировавших действий Ci, чтобы получить новый набор фиксировавших действий Ci+1. Демонстрировать, что это разумно для каждого Ci, мы должны демонстрировать выполнение Ei, содержащий Ci, который соблюдает определенные условия.

Формально, выполнение E удовлетворяет требования причинной связи модели памяти языка программирования Java, если и только если там существуют

Учитывая эти наборы действий C0... и выполнение E, sub> 1..., каждое действие в Ci должно быть одним из действий в Ei. Все действия в Ci должны совместно использовать того же самого родственника, происходит - перед порядком и порядком синхронизации и в Ei и в E. Формально,

  1. Ci является подмножеством Ая
  2. hbi |Ci = твердый черный |Ci
  3. специальная инструкция |Ci = так |Ci
Значения, записанные записями в Ci, должны быть тем же самым и в Ei и в E. Только чтения в Ci-1 должны видеть те же самые записи в Ei как в E. Формально,

  1. Вай |Ci = V |Ci
  2. Wi |Ci-1 = W |Ci-1
Все чтения в Ei, которые не находятся в Ci-1, должны видеть записи, которые происходят - перед ними. Каждое чтение r в Ci - Ci-1 должен видеть записи в Ci-1 и в Ei и в E, но может видеть различную запись в Ei от того, который это видит в E. Формально,

  1. Для любого чтения r в Ае - Ci-1, у нас есть hbi (Wi (r), r)
  2. Для любого чтения r в (Ci - Ci-1), у нас есть Wi (r) в Ci-1 и W (r) в Ci-1
Данный ряд достаточного синхронизируется - с краями для Ei, если есть выпуск - получают пару, которая происходит - прежде (§17.4.5) действие, которое Вы фиксируете, тогда та пара должна присутствовать во всем Эдже, где ji. Формально,

  1. Позвольте sswi быть swi краями, которые находятся также в переходном сокращении hbi, но не в почтовом. Мы вызываем sswi, который достаточное синхронизирует - с краями для Ei. Если sswi (x, y) и hbi (y, z) и z в Ci, то swj (x, y) для всего ji.
Если действие y фиксируется, все внешние действия, которые происходят - прежде y, также фиксируются.

  1. Если y находится в Ci, x является внешним действием и hbi (x, y), то x в Ci.


Обсуждение

Происходит - Прежде, чем непротиворечивость будет необходимым, но не достаточная, набор ограничений. Просто осуществление происходит - прежде, чем непротиворечивость учла бы недопустимые поведения - те, которые нарушают требования, которые мы установили для программ. Например, происходит - прежде, чем непротиворечивость позволит значениям появляться "из ничего". Это может быть замечено подробным исследованием Трассировки 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, мы не можем выполнить ни одну запись рано, потому что записи не могут произойти, если чтения не видят результат гонки данных.


17.4.9 Заметное Поведение и Незавершающееся Выполнение

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

Заметное поведение программы определяется конечными множествами внешних действий, которые может выполнить программа. Программа, которая, например, просто печатает "Привет" навсегда, описывается рядом поведений, что для любого неотрицательного целого числа 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 наблюдаются, но другие (внутренние) ограничения на то, как внешние действия сгенерированы и выполнены, может наложить такие ограничения.


17.5 Заключительная Полевая Семантика

Поля объявляются 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) выполняется

Global.s = "/tmp/usr".substring(4);
в то время как другой поток (распараллеливают 2) выполняется

String myS = Global.s;
if (myS.equals("/tmp"))System.out.println(myS);
String объекты предназначаются, чтобы быть неизменными, и строковые операции не выполняют синхронизацию. В то время как String у реализации нет никаких гонок данных, у другого кода могли быть гонки данных, включающие использование Strings, и модель памяти делает слабые гарантии программ, у которых есть гонки данных. В частности если поля String класс не был заключительным, тогда это будет возможно (хотя вряд ли), что Поток 2 мог первоначально видеть значение по умолчанию 0 для смещения строкового объекта, позволяя это сравниться как равный "/tmp". Более поздняя работа на String объект мог бы видеть корректное смещение 4, так, чтобы String объект воспринимается как являющийся "/usr". Много средств защиты языка программирования Java зависят от Strings воспринимаемый как действительно неизменный, даже если вредоносный код использует гонки данных, чтобы передать String ссылки между потоками.


17.5.1 Семантика Заключительных Полей

Семантика для заключительных полей следующие. Позвольте o быть объектом, и c быть конструктором для o, в котором пишется f. Действие замораживания на заключительном поле f o имеет место, когда c выходит, или обычно или резко.


Обсуждение

Отметьте, что, если один конструктор вызывает другого конструктора, и вызванный конструктор устанавливает заключительное поле, замораживание для заключительного поля имеет место в конце вызванного конструктора.


Для каждого выполнения поведение чтений под влиянием двух дополнительных частичных порядков, разыменовать цепочка разыменовывает () и мегагерц цепочки памяти (), которые, как полагают, являются частью выполнения (и таким образом, фиксируются для любого определенного выполнения). Эти частичные порядки должны удовлетворить следующие ограничения (у которого не должно быть уникального решения):

Учитывая запись w, замораживание f, действие (который не является чтением заключительного поля), чтение r1 заключительного поля, замороженного f и чтением r2 так, что твердый черный (w, f), твердый черный (f, a), мегагерц (a, r1) и, разыменовывает (r1, r2), затем определяя, какие значения могут быть замечены r2, мы считаем твердым черным (w, r2) (но эти упорядочивания не делают transitively соглашаются с другим, происходит - перед упорядочиваниями). Отметьте, что разыменовывает порядок, рефлексивно, и r1 может быть тем же самым как r2.

Для чтений заключительных полей единственные записи, которые, как считают, прибывают перед чтением заключительного поля, являются теми полученными через заключительную полевую семантику.

17.5.2 Чтение Заключительных Полей Во время Конструкции

Чтение заключительного поля объекта в пределах потока, который создает тот объект, упорядочивается относительно инициализации того поля в пределах конструктора обычным, происходит - перед правилами. Если чтение происходит после того, как поле устанавливается в конструкторе, это видит значение, заключительное поле присваивается, иначе это видит значение по умолчанию.

17.5.3 Последующая Модификация Заключительных Полей

В некоторых случаях, такие как десериализация, система должна будет изменить заключительные поля объекта после конструкции. Заключительные поля могут быть изменены через отражение и другие зависящие от реализации средства. Единственный образец, в котором у этого есть разумная семантика, является тем, в котором создается объект, и затем заключительные поля объекта обновляются. Объект не должен быть сделан видимым к другим потокам, и при этом заключительные поля не должны быть считаны, пока все обновления к заключительным полям объекта не полны. Замораживания заключительного поля происходят и в конце конструктора, в котором заключительное поле устанавливается, и сразу после каждой модификации заключительного поля через отражение или другой специальный механизм.

Даже тогда есть много сложностей. Если заключительное поле инициализируется ко времени компиляции, постоянному в полевом объявлении, изменения к заключительному полю не могут наблюдаться, так как использование того заключительного поля заменяется во время компиляции с постоянным временем компиляции.

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


Обсуждение

Например, рассмотрите следующий фрагмент кода:

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 не будет удалять заключительные полевые гарантии другого Runnables обработанный тем же самым исполнителем.

17.5.4 Поля Защищенные от записи

Обычно, заключительные статические поля не могут быть изменены. Однако System.in, System.out, и System.err заключительные статические поля, которым, по причинам наследства, нужно позволить быть измененными методами System.setIn, System.setOut и System.setErr. Мы именуем эти поля, как являющиеся защищенным от записи, чтобы отличить их от обычных заключительных полей.

Компилятор должен обработать эти поля по-другому от других заключительных полей. Например, чтение обычного заключительного поля "неуязвимо" к синхронизации: барьер, включенный в блокировку или энергозависимое чтение, не должен влиять на то, какое значение читается из заключительного поля. Так как значение полей защищенных от записи, как может замечаться, изменяется, события синхронизации должны иметь эффект на них. Поэтому, семантика диктуют, что эти поля обрабатываются как нормальные поля, которые не могут быть изменены пользовательским кодом, если тот пользовательский код не находится в System класс.

17.6 Word Tearing

Одно рассмотрение реализации для виртуальных машин Java состоит в том, что каждый элемент поля и массива считают отличным; обновления к одному полю или элементу не должны взаимодействовать с чтениями или обновлениями любого другого поля или элемента. В частности два потока, которые обновляют смежные элементы байтового массива отдельно, не должны вмешаться или взаимодействовать и не нуждаются в синхронизации, чтобы гарантировать последовательную непротиворечивость.

Некоторые процессоры не обеспечивают возможность записать в единственный байт. Это было бы недопустимо, чтобы реализовать обновления байтового массива о таком процессоре, просто читая все слово, обновляя соответствующий байт, и затем записывая все слово обратно к памяти. Эта проблема иногда известна как разрывание слова, и на процессорах, которые не могут легко обновить единственный байт в изоляции, некоторый другой подход будет требоваться.


Обсуждение

Вот прецедент, чтобы обнаружить разрывание слова:

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();
    }
}
Это делает точку, что байты не должны быть перезаписаны записями к смежным байтам


17.7 Неатомарная Обработка двойных и длинных

Некоторые реализации могут счесть удобным разделить единственное действие записи на 64-разрядном длинном или двойном значении в два действия записи на смежных 32 битовых значениях. Для пользы эффективности это поведение является определенной реализацией; виртуальные машины Java свободны выполнить записи к длинным и двойным значениям атомарно или в двух частях.

В целях модели памяти языка программирования Java единственная запись к энергонезависимому длинному или двойному значению обрабатывается как две отдельных записи: один к каждой 32-разрядной половине. Это может привести к ситуации, где поток видит первые 32 бита 64 битовых значений от одной записи, и вторые 32 бита от другой записи. Записи и чтения энергозависимых длинных и двойных значений являются всегда атомарными. Записи к и чтения ссылок являются всегда атомарными, независимо от того, реализуются ли они как 32 или 64 битовых значения.

Конструкторы VM поощряются избежать разделять их 64-разрядные значения где только возможно. Программисты поощряются объявить совместно использованные 64-разрядные значения как энергозависимые или синхронизировать их программы правильно, чтобы избежать возможных сложностей.

17.8 Ожидайте Наборы и Уведомление

17.8.1 Ожидать

Ожидайте действия происходят на вызов wait(), или синхронизированные формы wait(long millisecs) и wait(long millisecs, int nanosecs). Вызов wait(long millisecs) с параметром нуля, или вызовом wait(long millisecs, int nanosecs) с двумя нулевыми параметрами, эквивалентно вызову wait().

Поток обычно возвращается из a wait если это возвращается, не бросая InterruptedException.

Позвольте потоку t быть потоком, выполняющим ожидать метод на объектном м., и позволять n быть числом действий блокировки t на м., которые не были соответствующими, разблокировали действия. Одно из следующих действий происходит.

  1. Поток t добавляется к ожидать набору объектного м., и выполняет n, разблокировали действия на м.
  2. Поток t не выполняет дальнейшие инструкции, пока он не был удален из м., ожидают набор. Поток может быть удален из ожидать набора из-за любого из следующих действий, и возобновится когда-то позже.

    Каждый поток должен определить порядок по событиям, которые могли заставить его быть удаленным из ожидать набора. Тот порядок не должен быть непротиворечивым с другими упорядочиваниями, но поток должен вести себя, как если бы те события имели место в том порядке.

    Например, если поток t находится в ожидать наборе для м., и затем и прерывание t и уведомление о м. происходят, должен быть порядок по этим событиям.

    Если прерывание, как будут считать, произошло сначала, то t в конечном счете возвратится из wait бросая InterruptedException, и некоторый другой поток в ожидать наборе для м. (если кто-либо существует во время уведомления) должен получить уведомление. Если уведомление, как будут считать, произошло сначала, то t будет в конечном счете обычно возвращаться из wait с прерыванием, все еще ожидающим.

  3. Поток t выполняет действия блокировки n на м.
  4. Если поток t был удален из м., ожидают набор в шаге 2 из-за прерывания, состояние прерывания t устанавливается в ложь и ожидать броски метода InterruptedException.

17.8.2 Уведомление

Действия уведомления происходят на вызов методов notify и notifyAll. Позвольте потоку t быть потоком, выполняющим любой из этих методов на объектном м., и позволять n быть числом действий блокировки t на м., которые не были соответствующими, разблокировали действия. Одно из следующих действий происходит.

17.8.3 Прерывания

Действия прерывания происходят на вызов метода Thread.interrupt, так же как методы, определенные, чтобы вызвать это поочередно, такой как ThreadGroup.interrupt. Позвольте t быть вызовом u потока.interrupt, для некоторого потока u, где t и u могут быть тем же самым. Это действие заставляет состояние прерывания u быть установленным в истину.

Дополнительно, если там существует некоторый объектный м., чей ожидают, набор содержит u, u удаляется из м., ожидают набор. Это позволяет u возобновиться в ожидать действии, когда это ожидает после переблокировки монитора м., бросит InterruptedException.

Вызовы Thread.isInterrupted может определить состояние прерывания потока. Статический метод Thread.interrupted может быть вызван потоком, чтобы наблюдать и очистить его собственное состояние прерывания.

17.8.4 Взаимодействия Ожидают, Уведомление и Прерывание

Вышеупомянутые спецификации позволяют нам решать, что несколько свойств, имеющих отношение к взаимодействию, ожидают, уведомление и прерывание. Если поток и уведомляется и прерывается, ожидая, он может также:

Поток, возможно, не сбрасывает свое состояние прерывания и обычно возвращается из звонка wait.

Точно так же уведомления не могут быть потеряны из-за прерываний. Предположите, что набор s потоков находится в ожидать наборе объектного м., и другой поток выполняет a notify на м. Затем также

Отметьте что, если поток и прерывается и будится через notify, и тот поток возвращается из wait бросая InterruptedException, тогда некоторый другой поток в ожидать наборе должен быть уведомлен.

17.9 Сон и Урожай

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. Все права защищены
Пожалуйста, отправьте любые комментарии или исправления через нашу форму обратной связи

free hit counter