Spec-Zone.ru › Java Language Specification 17

Глава 17. Потоки и блокировки

Содержание

17.1. Синхронизация
17.2. Наборы ожиданий и уведомления
17.2.1. Ожидание
17.2.2. Уведомление
17.2.3. Прерывания
17.2.4. Взаимодействие ожиданий, уведомлений и прерываний
17.3. Сон и уступка
17.4. Модель памяти
17.4.1. Общие переменные
17.4.2. Действия
17.4.3. Программы и порядок выполнения
17.4.4. Порядок синхронизации
17.4.5. Порядок произошедших событий
17.4.6. Выполнения
17.4.7. Правильные выполнения
17.4.8. Выполнения и требования к причинности
17.4.9. Наблюдаемое поведение и бесконечные выполнения
17.5. final Семантика полей
17.5.1. Семантика полей final
17.5.2. Чтение полей final во время создания
17.5.3. Последующее изменение полей final
17.5.4. Защищённые от записи поля
17.6. Разрыв слова
17.7. Неатомарная обработка double и long

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

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

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

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

17.1. Синхронизация

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

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

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

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

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

17.2. Наборы ожидания и уведомления

Каждый объект, помимо связанного монитора, имеет связанный набор ожидания. Набор ожидания — это множество потоков.

Когда объект создается впервые, его набор ожидания пуст. Элементарные действия, добавляющие и удаляющие потоки из наборов ожидания, атомарны. Наборы ожидания управляются исключительно методами Object.wait, Object.notify и Object.notifyAll.

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

17.2.1. Ожидание

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

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

Поток возвращается нормально из ожидания, если он возвращается без выбрасывания InterruptedException.

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

  • Если n равно нулю (т.е., поток t не обладает блокировкой для целевого объекта m), то выбрасывается IllegalMonitorStateException.

  • Если это временное ожидание, и аргумент nanosecs не находится в диапазоне 0-999999 или аргумент millisecs отрицателен, то выбрасывается IllegalArgumentException.

  • Если поток t прерван, то выбрасывается InterruptedException, и состояние прерывания потока t устанавливается в ложь.

  • В противном случае происходит следующая последовательность:

    1. Поток t добавляется в набор ожидания объекта m, и выполняет n действий разблокировки на m.

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

      • Выполнение действия notify на m, при котором t выбирается для удаления из набора ожидания.

      • Выполнение действия notifyAll на m.

      • Выполнение действия interrupt на t.

      • Если это временное ожидание, внутреннее действие, удаляющее t из набора ожидания m, происходит по истечении как минимум millisecs миллисекунд плюс nanosecs наносекунд с момента начала действия ожидания.

      • Внутреннее действие реализации. Реализации разрешено, хотя и не рекомендуется, выполнять "ложные пробуждения", т.е. удалять потоки из наборов ожидания и тем самым разрешать возобновление без явных инструкций.

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

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

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

    3. Поток t выполняет n действий блокировки на m.

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

17.2.2. Уведомление

Действия уведомления происходят при вызове методов notify и notifyAll.

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

  • Если n равно нулю, то выбрасывается IllegalMonitorStateException.

    В этом случае поток t не обладает блокировкой для целевого объекта m.

  • Если n больше нуля, и это действие notify, то если набор ожидания m не пуст, выбирается и удаляется из набора ожидания поток u, являющийся членом текущего набора ожидания m.

    Нет гарантии, какой поток из набора ожидания будет выбран. Это удаление из набора ожидания разрешает возобновление u в действии ожидания. Обратите внимание, однако, что действия блокировки u при возобновлении не могут произойти до некоторого времени после того, как t полностью разблокирует монитор для m.

  • Если n больше нуля, и это действие notifyAll, то все потоки удаляются из набора ожидания m и, таким образом, возобновляются.

    Обратите внимание, однако, что только один из них одновременно сможет заблокировать монитор, необходимый во время возобновления ожидания.

17.2.3. Прерывания

Действия прерывания происходят при вызове Thread.interrupt, а также методов, определяющих его вызов, таких как ThreadGroup.interrupt.

Пусть t — поток, вызывающий u.interrupt для некоторого потока u, где t и u могут быть одинаковыми. Это действие приводит к установке состояния прерывания u в истинное.

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

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

17.2.4. Взаимодействие ожиданий, уведомлений и прерывания

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

Если поток как уведомлен, так и прерван во время ожидания, он может:

  • возвратиться нормально из wait, сохраняя при этом ожидающее прерывание (другими словами, вызов Thread.interrupted вернет true)

  • вернуться из wait, выбросив исключение InterruptedException

Поток не может сбросить свой статус прерывания и вернуться нормально из вызова wait.

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

  • по крайней мере один поток в s должен вернуться нормально из wait, или

  • все потоки в s должны выйти из wait, выбросив исключение InterruptedException.

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

17.3. Сон и уступка

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

Важно отметить, что ни Thread.sleep, ни Thread.yield не имеют семантики синхронизации. В частности, компилятор не обязан выгружать записи, кэшированные в регистрах, в общую память перед вызовом Thread.sleep или Thread.yield, а также компилятор не обязан перезагружать значения, кэшированные в регистрах, после вызова Thread.sleep или Thread.yield.

Например, в следующем (нерабочем) фрагменте кода предположим, что this.done является полем, не являющимся volatile boolean:

while (!this.done)
    Thread.sleep(1000);

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

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

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

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

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

Пример 17.4-1. Неправильно синхронизированные программы могут демонстрировать неожиданное поведение

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

Рассмотрим, например, примеры программных трассировок, представленных в таблице 17.4-A. Эта программа использует локальные переменные r1 и r2 и общие переменные A и B. Изначально, A == B == 0.

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

Поток 1 Поток 2
1: r2 = A; 3: r1 = B;
2: B = 1; 4: A = 2;

Может показаться, что результат r2 == 2 и r1 == 1 невозможен. Интуитивно, либо оператор 1, либо оператор 3 должны быть первыми в ходе выполнения. Если оператор 1 выполняется первым, он не должен видеть запись в операторе 4. Если оператор 3 выполняется первым, он не должен видеть запись в операторе 2.

Если какое-либо выполнение продемонстрировало это поведение, то мы бы знали, что оператор 4 выполнился перед оператором 1, который выполнился перед оператором 2, который выполнился перед оператором 3, который выполнился перед оператором 4. Это, на первый взгляд, абсурдно.

Однако компиляторы могут переупорядочивать инструкции в любом потоке, если это не влияет на выполнение этого потока изолированно. Если оператор 1 переупорядочен с оператором 2, как показано в трассировке в таблице 17.4-B, то легко понять, как может произойти результат r2 == 2 и r1 == 1.

Таблица 17.4-B. Неожиданные результаты, вызванные переупорядочением операторов - допустимое преобразование компилятора

Поток 1 Поток 2
B = 1; r1 = B;
r2 = A; A = 2;

Для некоторых программистов такое поведение может показаться "сломанным". Однако следует отметить, что этот код некорректно синхронизирован:

  • есть запись в одном потоке,

  • чтение той же переменной другим потоком,

  • и запись и чтение не упорядочены синхронизацией.

Эта ситуация является примером гонки за ресурсом (§17.4.5). Когда код содержит гонку за ресурсом, часто возможны неинтуитивные результаты.

Несколько механизмов могут привести к переупорядочению в таблице 17.4-B. Компилятор Just-In-Time в реализации Java Virtual Machine может переупорядочить код, или процессор. Кроме того, иерархия памяти архитектуры, на которой выполняется реализация Java Virtual Machine, может создать видимость переупорядочения кода. В этой главе мы будем называть всё, что может переупорядочивать код, компилятором.

Другой пример неожиданных результатов можно увидеть в таблице 17.4-C. Изначально, p == q и p.x == 0. Эта программа также некорректно синхронизирована; она записывает в общую память без принудительного упорядочения этих записей.

Таблица 17.4-C. Неожиданные результаты, вызванные подстановкой вперёд

Поток 1 Поток 2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;
r4 = r3.x;
r5 = r1.x;

Одна распространённая оптимизация компилятора заключается в повторном использовании значения, считанного для r2, для r5: оба являются чтениями r1.x без промежуточных записей. Эта ситуация показана в таблице 17.4-D.

Таблица 17.4-D. Неожиданные результаты, вызванные подстановкой вперёд

Поток 1 Поток 2
r1 = p; r6 = p;
r2 = r1.x; r6.x = 3;
r3 = q;
r4 = r3.x;
r5 = r2;

Теперь рассмотрим случай, когда присвоение r6.x в Потоке 2 происходит между первым чтением r1.x и чтением r3.x в Потоке 1. Если компилятор решит переиспользовать значение r2 для r5, то r2 и r5 будут иметь значение 0, а r4 будет иметь значение 3. С точки зрения программиста, значение, хранящееся в p.x, изменилось с 0 на 3, а затем вернулось обратно.


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

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

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

Указанная здесь модель памяти не основана фундаментально на объектно-ориентированной природе языка программирования Java. Для краткости и простоты в наших примерах мы часто показываем фрагменты кода без определений классов или методов, или явного разыменования. Большинство примеров состоят из двух или более потоков, содержащих операторы с доступом к локальным переменным, общим глобальным переменным или полям экземпляра объекта. Мы обычно используем имена переменных, такие как r1 или r2, чтобы обозначить переменные, локальные для метода или потока. Такие переменные недоступны другим потокам.

17.4.1. Общие переменные

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

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

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

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

17.4.2. Действия

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

  • Чтение (обычное или не-volatile). Чтение переменной.

  • Запись (обычное или не-volatile). Запись переменной.

  • Действия синхронизации, которые включают:

    • Чтение volatile. Чтение volatile переменной.

    • Запись volatile. Запись volatile переменной.

    • Блокировка. Блокировка монитора.

    • Разблокировка. Разблокировка монитора.

    • Первое и последнее действие потока (синтетические).

    • Действия, которые запускают поток или обнаруживают завершение потока (§17.4.4).

  • Внешние действия. Внешнее действие — это действие, которое может быть наблюдаемо за пределами выполнения и имеет результат, основанный на среде, внешней по отношению к выполнению.

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

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

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

Действие a описывается кортежем < t, k, v, u >, состоящим из:

  • t — поток, выполняющий действие

  • k — вид действия

  • v — переменная или монитор, участвующие в действии.

    Для действий блокировки v — это монитор, который блокируется; для действий разблокировки v — это монитор, который разблокируется.

    Если действие представляет собой (volatile или non-volatile) чтение, v — это переменная, которая читается.

    Если действие представляет собой (volatile или non-volatile) запись, v — это переменная, которая записывается.

  • u — произвольный уникальный идентификатор действия

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

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

При выполнении бесконечных циклов не все внешние действия наблюдаемы. Бесконечные циклы и наблюдаемые действия обсуждаются в §17.4.9.

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

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

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

  • w предшествует r в порядке выполнения, и

  • нет другой записи w' такой, что w предшествует w' и w' предшествует r в порядке выполнения.

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

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

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

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

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

Каждое выполнение имеет порядок синхронизации. Порядок синхронизации — это общий порядок всех действий синхронизации в выполнении. Для каждой нити t порядок синхронизации действий синхронизации (§17.4.2) в t согласуется с порядком программы (§17.4.3) нити t.

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

  • Действие разблокировки монитора m synchronizes-with все последующие действия блокировки на m (где «последующие» определяются в соответствии с порядком синхронизации).

  • Запись в переменную volatile v (§8.3.1.4) synchronizes-with все последующие чтения v любой нитью (где «последующие» определяются в соответствии с порядком синхронизации).

  • Действие запуска нити synchronizes-with первое действие в запущенной нити.

  • Запись значения по умолчанию (ноль, false или null) для каждой переменной synchronizes-with первое действие в каждой нити.

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

  • Финальное действие в нити T1 synchronizes-with любое действие в другой нити T2, которая обнаруживает, что T1 завершилась.

    T2 может сделать это, вызвав T1.isAlive() или T1.join().

  • Если нить T1 прерывает нить T2, прерывание от T1 synchronizes-with любой момент, когда любая другая нить (включая T2) определяет, что T2 была прервана (через брошенный InterruptedException или вызов Thread.interrupted или Thread.isInterrupted).

Источник synchronizes-with связи называется освобождением, а место назначения — приобретением.

17.4.5. Порядок «happens-before»

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

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

  • Если x и y — действия одного потока, и x предшествует y в порядке программы, то hb(x, y).

  • Существует ребро happens-before от конца конструктора объекта к началу финализатора (§12.6) для этого объекта.

  • Если действие x synchronizes-with последующее действие y, то также hb(x, y).

  • Если hb(x, y) и hb(y, z), то hb(x, z).

Методы wait класса Object (§17.2.1) имеют связанные с ними действия блокировки и разблокировки; их отношения happens-before определяются этими связанными действиями.

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

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

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

Отношение happens-before определяет, когда происходят гонки данных.

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

Из вышеперечисленных определений следует, что:

  • Разблокировка монитора happens-before всякой последующей блокировки этого монитора.

  • Запись в поле volatile (§8.3.1.4) happens-before всякого последующего чтения этого поля.

  • Вызов start() в потоке happens-before любых действий в запущенном потоке.

  • Все действия в потоке happens-before любому другому потоку, который успешно возвращается из join() в этом потоке.

  • Инициализация по умолчанию любого объекта happens-before любых других действий (кроме записей по умолчанию) программы.

Когда программа содержит два конфликтующих доступа (§17.4.1), которые не упорядочены отношением happens-before, говорят, что программа содержит гонку данных.

Семантика операций, отличных от межпоточных действий, таких как чтение длин массивов (§10.7), выполнения проверок типов (§5.5, §15.16) и вызова виртуальных методов (§15.12), непосредственно не затрагиваются гонками данных.

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

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

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

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

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

Мы говорим, что чтение r переменной v может наблюдать запись w в v, если в частичном порядке happens-before следа выполнения:

  • r не упорядочено перед w (т. е. не выполняется hb(r, w)), и

  • нет промежуточной записи w' в v (т. е. нет записи w' в v, такой что hb(w, w') и hb(w', r)).

Неформально, чтение r может увидеть результат записи w, если нет упорядочения happens-before, препятствующего этому чтению.

Набор действий A является согласованным по happens-before, если для всех чтений r в A, где W(r) — действие записи, видимое r, не выполняется ни hb(r, W(r)), ни то, что существует запись w в A такая, что w.v = r.v и hb(W(r), w) и hb(w, r).

В наборе действий, согласованном по happens-before, каждое чтение видит запись, которую оно может увидеть согласно упорядочению happens-before.

Пример 17.4.5-1. Согласованность по happens-before

Для следа в Таблице 17.4.5-A, изначально A == B == 0. След может наблюдать r2 == 0 и r1 == 0 и всё ещё быть согласованным по happens-before, так как существуют порядки выполнения, которые позволяют каждому чтению увидеть соответствующую запись.

Таблица 17.4.5-A. Поведение, разрешенное согласованностью happens-before, но не последовательной согласованностью.

Поток 1 Поток 2
B = 1; A = 2;
r2 = A; r1 = B;

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

1: B = 1;
3: A = 2;
2: r2 = A;  // sees initial write of 0
4: r1 = B;  // sees initial write of 0

Другой порядок выполнения, который согласован по happens-before, таков:

1: r2 = A;  // sees write of A = 2
3: r1 = B;  // sees write of B = 1
2: B = 1;
4: A = 2;

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


17.4.6. Выполнения

Выполнение E описывается кортежем < P, A, po, so, W, V, sw, hb >, включающим:

  • P - программа

  • A - набор действий

  • po - порядок выполнения программы, который для каждой нити t представляет собой полное упорядочение всех действий, выполненных нитью t в A

  • so - порядок синхронизации, который представляет собой полное упорядочение всех действий синхронизации в A

  • W - функция записи, которая для каждого чтения r в A возвращает W(r), действие записи, увиденное чтением r в E.

  • V - функция значения, которая для каждой записи w в A возвращает V(w), значение, записанное записью w в E.

  • sw - синхронизируется с, частичный порядок над действиями синхронизации

  • hb - происходит до, частичный порядок над действиями

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

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

17.4.7. Корректные выполнения

Мы рассматриваем только корректные выполнения. Выполнение E = < P, A, po, so, W, V, sw, hb > является корректным, если следующие утверждения верны:

  1. Каждое чтение видит запись в ту же переменную в выполнении.

    Все чтения и записи переменных типа volatile являются действиями volatile. Для всех чтений r в A у нас есть W(r) в A и W(r).v = r.v. Переменная r.v является volatile тогда и только тогда, когда r является volatile чтением, и переменная w.v является volatile тогда и только тогда, когда w является volatile записью.

  2. Порядок происходит-до является частичным порядком.

    Порядок происходит-до определяется транзитивным замыканием ребер синхронизируется-с и порядка выполнения. Он должен быть допустимым частичным порядком: рефлексивным, транзитивным и антисимметричным.

  3. Выполнение подчиняется согласованности внутри нитей.

    Для каждой нити t действия, выполненные нитью t в A, такие же, как были бы сгенерированы этой нитью в порядке выполнения программы изолированно, при каждой записи w, записывающей значение V(w), учитывая, что каждое чтение r видит значение V(W(r)). Значения, видимые каждым чтением, определяются моделью памяти. Указанный порядок выполнения должен отражать порядок выполнения программы в соответствии с внутри-нитевой семантикой P.

  4. Выполнение является согласованным по принципу происходит-до (§17.4.6).

  5. Выполнение подчиняется согласованности порядка синхронизации.

    Для всех volatile чтений r в A, неверно, что либо so(r, W(r)), либо существует запись w в A, такая что w.v = r.v и so(W(r), w) и so(w, r).

Требования к выполнению и причинно-следственной связи

Мы используем 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). Если x или y не входят в d, то p|d(x,y) не выполняется.

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

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

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

  • Множества действий C0, C1, ... такие, что:

    • C0 — пустое множество

    • Ci является собственным подмножеством Ci+1

    • A = ∪ (C0, C1, ...)

    Если A конечно, то последовательность C0, C1, ... будет конечной, заканчиваясь множеством Cn = A.

    Если A бесконечно, то последовательность C0, C1, ... может быть бесконечной, и должно быть так, что объединение всех элементов этой бесконечной последовательности равно A.

  • Правильные выполнения E1, ..., где Ei = < P, Ai, poi, soi, Wi, Vi, swi, hbi >.

Учитывая эти множества действий C0, ... и выполнения E1, ..., каждое действие в Ci должно быть одним из действий в Ei. Все действия в Ci должны разделять тот же относительный порядок «происходит до» и порядок синхронизации как в Ei, так и в E. Формально:

  1. Ci является подмножеством Ai

  2. hbi|Ci = hb|Ci

  3. soi|Ci = so|Ci

Значения, записанные записями в Ci, должны быть теми же в Ei и E. Только чтения в Ci-1 должны видеть те же записи в Ei, что и в E. Формально:

  1. Vi|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 в Ai - Ci-1 у нас есть hbi(Wi(r), r)

  2. Для любого чтения r в (Ci - Ci-1), у нас есть Wi(r) в Ci-1 и W(r) в Ci-1

Учитывая множество достаточных связей «синхронизируется с» для Ei, если есть пара «выпуск-получение», которая происходит до (§17.4.5) действия, которое вы фиксируете, то эта пара должна присутствовать во всех Ej, где j ≥ i. Формально:

  1. Пусть sswi — это ребра swi, которые также находятся в транзитивном сокращении hbi, но не в po. Мы называем sswi достаточными ребрами «синхронизируется с» для Ei. Если sswi(x, y) и hbi(y, z) и z в Ci, то swj(x, y) для всех j ≥ i.

    Если действие y зафиксировано, все внешние действия, которые происходят до y, также фиксируются.

  2. Если y в Ci, x — внешнее действие и hbi(x, y), то x в Ci.

Пример 17.4.8-1. Согласованность happens-before недостаточна

Согласованность happens-before является необходимым, но не достаточным набором ограничений. Простое применение согласованности happens-before позволило бы недопустимые действия — те, которые нарушают требования, установленные для программ. Например, согласованность happens-before позволяет значениям появляться «из ничего». Это можно увидеть, подробно рассмотрев трассировку в таблице 17.4.8-A.

Таблица 17.4.8-A. Согласованность happens-before недостаточна

Поток 1 Поток 2
r1 = x; r2 = y;
if (r1 != 0) y = 1; if (r2 != 0) x = 1;

Код, показанный в таблице 17.4.8-A, синхронизирован правильно. Это может показаться неожиданным, поскольку он не выполняет никаких действий синхронизации. Однако помните, что программа синхронизирована правильно, если при её выполнении в последовательно-согласованном режиме нет гонок данных. Если этот код выполняется последовательно-согласованным образом, каждое действие произойдёт в порядке программы, и ни одно из записей не произойдёт. Поскольку записей не происходит, гонок данных быть не может: программа синхронизирована правильно.

Поскольку эта программа синхронизирована правильно, единственные разрешенные действия — последовательно-согласованные. Однако существует выполнение этой программы, которое согласовано по happens-before, но не последовательно:

r1 = x;  // sees write of x = 1
y = 1;
r2 = y;  // sees write of y = 1
x = 1; 

Этот результат согласован по happens-before: нет отношения happens-before, которое препятствовало бы его возникновению. Однако это явно неприемлемо: нет последовательно-согласованного выполнения, которое привело бы к такому поведению. Факт, что чтение может увидеть запись, которая идёт позже в порядке выполнения, иногда может привести к недопустимым действиям.

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

Мы называем проблему, когда чтения могут видеть будущие записи, причинностью, из-за проблем, возникающих в таких случаях, как тот, который можно найти в таблице 17.4.8-A. В этом случае чтения вызывают записи, а записи вызывают чтения. Нет «первопричины» для действий. Таким образом, нашей модели памяти нужен согласованный способ определения, какие чтения могут видеть ранние записи.

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

Модель памяти принимает на вход заданное выполнение и программу и определяет, является ли это выполнение законным выполнением программы. Она делает это, постепенно строя набор «зафиксированных» действий, которые отражают, какие действия были выполнены программой. Обычно следующее действие, которое будет зафиксировано, отражает следующее действие, которое может быть выполнено последовательно-согласованным выполнением. Однако, чтобы отразить чтения, которые должны видеть последующие записи, мы разрешаем некоторым действиям фиксироваться раньше, чем другие действия, которые предшествуют им по happens-before.

Очевидно, что некоторые действия могут быть зафиксированы раньше, а некоторые — нет. Если, например, одна из записей в таблице 17.4.8-A была зафиксирована до чтения этой переменной, чтение могло бы увидеть запись, и мог бы произойти результат «из ничего». Неформально, мы разрешаем действие зафиксировать раньше, если знаем, что действие может произойти без предположения о том, что произошла гонка данных. В таблице 17.4.8-A мы не можем выполнить ни одну запись раньше, потому что записи не могут произойти, пока чтения не увидят результат гонки данных.


17.4.9. Наблюдаемое поведение и бесконечные исполнения

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

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

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

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

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

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

Чтобы рассуждать о наблюдаемых поведении, нам нужно говорить о множествах наблюдаемых действий.

Если O — множество наблюдаемых действий для выполнения E, то множество O должно быть подмножеством действий E, A, и должно содержать только конечное число действий, даже если A содержит бесконечное число действий. Кроме того, если действие y находится в O, и либо hb(x, y), либо so(x, y), то x находится в O.

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

Поведение B является допустимым поведением программы P тогда и только тогда, когда B является конечным множеством внешних действий и либо:

  • Существует выполнение E программы P, и множество O наблюдаемых действий для E, и B является множеством внешних действий в O (Если какие-либо потоки в E завершаются в заблокированном состоянии и O содержит все действия в E, то B также может содержать действие hang); или

  • Существует множество O действий, такое что B состоит из действия hang плюс всех внешних действий в O и для всех k ≥ | O |, существует выполнение E программы P с действиями A, и существует множество действий O' такое что:

    • И O, и O' являются подмножествами A, которые удовлетворяют требованиям к множествам наблюдаемых действий.

    • O ⊆ O' ⊆ A

    • | O' | ≥ k

    • O' - O не содержит внешних действий

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

17.5. final Семантика полей

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

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

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

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

Пример 17.5-1. final Поля в модели памяти Java

Программа ниже иллюстрирует, как 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 имеет final int поле x и обычное поле int поле y. Один поток может выполнить метод writer, а другой — метод reader.

Поскольку метод writer записывает f после завершения конструктора объекта, метод reader будет гарантированно видеть правильно инициализированное значение для f.x: он прочитает значение 3. Однако, f.y не является final; метод reader поэтому не гарантирует, что увидит значение 4 для него.


Пример 17.5-2. final Поля для безопасности

final поля предназначены для обеспечения необходимых гарантий безопасности. Рассмотрим следующую программу. Один поток (который мы будем называть потоком 1) выполняет:

Global.s = "/tmp/usr".substring(4);

в то время как другой поток (поток 2) выполняет

String myS = Global.s; 
if (myS.equals("/tmp"))System.out.println(myS);

String объекты предназначены для неизменяемости, а операции со строками не выполняют синхронизацию. Хотя реализация String не содержит гонок данных, другой код может иметь гонки данных, связанные с использованием String объектов, и модель памяти предоставляет слабые гарантии для программ, которые имеют гонки данных. В частности, если поля класса String не были final, то было бы возможно (хотя и маловероятно), что поток 2 первоначально увидит значение по умолчанию для смещения объекта строки, позволяя сравнить его как равное "/tmp". Позже операция с объектом String может увидеть правильное смещение 4, так что объект String воспринимается как "/usr". Многие функции безопасности языка программирования Java зависят от того, чтобы объекты String воспринимались как действительно неизменяемые, даже если вредоносный код использует гонки данных для передачи String ссылок между потоками.


17.5.1. Семантика final Полей

Пусть o — объект, а c — конструктор для o, в котором записывается final поле f. Действие замораживания final поля f объекта o происходит при выходе c, как нормальным, так и аварийным способом.

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

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

  • Цепочка ссылок: Если действие a — чтение или запись поля или элемента объекта o потоком t, который не инициализировал o, то должно существовать некоторое чтение r потоком t, которое видит адрес o, такое, что r dereferences(r, a).

  • Цепочка памяти: Существуют несколько ограничений на порядок цепочки памяти:

    • Если r — чтение, которое видит запись w, то должно выполняться mc(w, r).

    • Если r и a — действия, такие, что dereferences(r, a), то должно выполняться mc(r, a).

    • Если w — запись адреса объекта o потоком t, который не инициализировал o, то должно существовать некоторое чтение r потоком t, которое видит адрес o, такое, что mc(r, w).

Учитывая запись w, замораживание f, действие a (которое не является чтением final поля), чтение r1 final поля, замороженного f, и чтение r2, такое, что hb(w, f), hb(f, a), mc(a, r1) и dereferences(r1, r2), то при определении, какие значения могут быть прочитаны r2, мы рассматриваем hb(w, r2). (Этот happens-before порядок не транзитивно закрывается с другими happens-before порядками.)

Обратите внимание, что порядок dereferences рефлексивен, и r1 может быть таким же, как r2.

Для чтений final полей, единственными записями, которые считаются предшествующими чтению final поля, являются те, которые получены через семантику final поля.

17.5.2. Чтение final Полей во время построения

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

17.5.3. Последующее изменение полей final

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

Даже тогда существует ряд сложностей. Если поле final инициализируется константным выражением (§15.29) в объявлении поля, изменения поля final могут не быть замечены, так как использование этого поля final заменяется на значение константного выражения на этапе компиляции.

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

Пример 17.5.3-1. Агрессивная оптимизация полей final

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. Таким образом, new A().f() может вернуть -1, 0 или 1.


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

final-безопасный контекст для полей имеет дополнительные защиты. Если поток увидел неправильно опубликованную ссылку на объект, которая позволяет потоку увидеть значение по умолчанию поля final, а затем в final-безопасном контексте для полей прочитает правильно опубликованную ссылку на объект, ему гарантированно будет показано правильное значение поля final. В формализме код, исполняемый в final-безопасном контексте для полей, рассматривается как отдельный поток (только для целей семантики поля final).

В реализации компилятор не должен перемещать доступ к полю final в или из final-безопасного контекста для полей (хотя он может быть перемещён вокруг выполнения такого контекста, если объект не создаётся в этом контексте).

Одно место, где использование final-безопасного контекста для полей будет уместно, — это в исполнителе или пуле потоков. Выполняя каждый Runnable в отдельном final-безопасном контексте для полей, исполнителю можно гарантировать, что неправильный доступ одного Runnable к объекту o не лишит final гарантий для других Runnable, обрабатываемых тем же исполнителем.

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

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

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

17.6. Разрыв слова

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

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

Пример 17.6-1. Обнаружение разрыва слова

Следующая программа является тестовым случаем для обнаружения разрыва слова:

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. Неатомарное обращение с double и long

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

Записи и чтения volatile значений long и double всегда атомарны.

Записи и чтения ссылок всегда атомарны, независимо от того, реализуются ли они как 32-битные или 64-битные значения.

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

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

© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API