Глава 17. Потоки и блокировки
Содержание
- 17.1. Синхронизация
- 17.2. Наборы ожидания и уведомления
- 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.6. Разрывы слов
- 17.7. Неатомарная обработка
doubleиlong
В то время как большая часть обсуждения в предыдущих главах касается только поведения кода, выполняемого по одной инструкции или выражению за раз, то есть одним потоком, виртуальная машина Java может поддерживать сразу множество потоков выполнения. Эти потоки независимо выполняют код, работающий с значениями и объектами, находящимися в общей памяти. Потоки могут поддерживаться множеством аппаратных процессоров, путём разделения времени на одном аппаратном процессоре или путём разделения времени на многих аппаратных процессорах.
Потоки представлены классом Thread. Единственный способ для пользователя создать поток – создать объект этого класса; каждый поток связан с таким объектом. Поток начнётся, когда метод start() будет вызван на соответствующем объекте Thread.
Поведение потоков, особенно при неправильной синхронизации, может быть запутанным и неинтуитивным. В этой главе описаны семантика многопоточных программ; она включает правила, определяющие, какие значения могут быть замечены чтением общей памяти, обновляемой несколькими потоками. Поскольку спецификация похожа на модели памяти для различных архитектур оборудования, эти семантика известны как модель памяти языка программирования Java. Когда путаница не может возникнуть, мы будем просто ссылаться на эти правила как на «модель памяти».
Эти семантика не предписывают, как должно выполняться многопоточная программа. Скорее, они описывают поведение, которое могут демонстрировать многопоточные программы. Любая стратегия выполнения, которая генерирует только разрешённое поведение, является приемлемой стратегией выполнения.
Язык программирования Java предоставляет несколько механизмов для взаимодействия между потоками. Наиболее базовый из этих методов – это синхронизация, которая реализована с использованием мониторов. Каждый объект в Java связан с монитором, который поток может заблокировать или разблокировать. Только один поток одновременно может удерживать блокировку на мониторе. Любые другие потоки, пытающиеся заблокировать этот монитор, блокируются до тех пор, пока не смогут получить блокировку на этот монитор. Поток t может блокировать определённый монитор несколько раз; каждое разблокирование отменяет действие одной операции блокировки.
Инструкция synchronized (§14.19) вычисляет ссылку на объект; затем она пытается выполнить действие блокировки на мониторе этого объекта и не продолжает выполнение до тех пор, пока действие блокировки не будет успешно завершено. После выполнения действия блокировки выполняется тело инструкции synchronized. Если выполнение тела когда-либо завершится, либо нормально, либо прерванно, автоматически выполняется действие разблокирования на том же самом мониторе.
Метод synchronized (§8.4.3.6) автоматически выполняет действие блокировки при вызове; его тело не выполняется до тех пор, пока действие блокировки не будет успешно завершено. Если метод является методом экземпляра, он блокирует монитор, связанный с экземпляром, для которого он был вызван (то есть объект, который будет известен как this во время выполнения тела метода). Если метод является static, он блокирует монитор, связанный с объектом Class, который представляет класс, в котором определён метод. Если выполнение тела метода когда-либо завершится, либо нормально, либо прерванно, автоматически выполняется действие разблокирования на том же самом мониторе.
Язык программирования Java не предотвращает и не требует выявления условий тупика. Программы, в которых потоки удерживают (прямо или косвенно) блокировки на нескольких объектах, должны использовать стандартные методы для предотвращения тупика, создавая, при необходимости, более высокие уровни примитивов блокировки, которые не приводят к тупику.
Другие механизмы, такие как чтение и запись переменных volatile и использование классов в пакете java.util.concurrent, предоставляют альтернативные способы синхронизации.
Каждый объект, помимо связанного монитора, имеет связанный набор ожиданий. Набор ожиданий — это набор потоков.
Когда объект создается впервые, его набор ожиданий пустой. Элементарные действия, добавляющие и удаляющие потоки из наборов ожиданий, являются атомарными. Наборы ожиданий управляются только с помощью методов Object.wait, Object.notify и Object.notifyAll.
Действия с наборами ожиданий также могут зависеть от состояния прерывания потока и от методов класса Thread, связанных с прерыванием. Кроме того, методы класса Thread для сна и присоединения к другим потокам обладают свойствами, полученными от действий ожидания и уведомления.
Действия ожидания происходят при вызове 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 устанавливается в ложь. -
В противном случае происходит следующая последовательность:
-
Поток t добавляется в набор ожиданий объекта m, и выполняет n действий разблокировки на m.
-
Поток 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с ещё ожидающим прерыванием. -
-
Поток t выполняет n действий блокировки на m.
-
Если поток t был удален из набора ожиданий m на шаге 2 из-за прерывания, то состояние прерывания t устанавливается в ложь, и метод
waitвыбрасываетInterruptedException.
-
Действия уведомления происходят при вызове методов notify и notifyAll.
Пусть поток t выполняет любой из этих методов на объекте m, а n — количество действий блокировки потоком t на m, не сопоставленных действиями разблокировки. Происходит одно из следующих действий:
-
Если n равно нулю, то выбрасывается
IllegalMonitorStateException.Это случай, когда поток t не владеет блокировкой объекта m.
-
Если n больше нуля и это действие
notify, то если набор ожиданий m не пустой, выбирается и удаляется из набора ожиданий поток u, который является членом текущего набора ожиданий m.Нет гарантии, какой поток из набора ожиданий будет выбран. Это удаление из набора ожиданий позволяет u возобновить действие ожидания. Однако обратите внимание, что действия блокировки u при возобновлении не могут быть успешными, пока t полностью не разблокирует монитор для m.
-
Если n больше нуля и это действие
notifyAll, то все потоки удаляются из набора ожиданий m, и, таким образом, возобновляются.Однако обратите внимание, что только один из них за раз заблокирует монитор, необходимый во время возобновления ожидания.
Действия прерывания происходят при вызове Thread.interrupt, а также методов, определённых для его вызова, таких как ThreadGroup.interrupt.
Пусть t — поток, вызывающий u.interrupt для некоторого потока u, где t и u могут быть одинаковыми. Это действие приводит к установке состояния прерывания u в истинное.
Кроме того, если существует объект m, чей набор ожиданий содержит u, то u удаляется из набора ожиданий m. Это позволяет u возобновить действие ожидания, в результате чего это ожидание, после повторного блокирования монитора m, выбросит InterruptedException.
Вызовы Thread.isInterrupted могут определять состояние прерывания потока. Метод static Thread.interrupted может вызываться потоком для наблюдения и сброса собственного состояния прерывания.
Вышеуказанные спецификации позволяют определить несколько свойств, связанных с взаимодействием ожиданий, уведомлений и прерываний.
Если поток прерывается и получает уведомление во время ожидания, он может:
-
нормально завершить
wait, при этом прерывание останется ожидающим (другими словами, вызовThread.interruptedвернёт true) -
завершиться из
wait, сбросивInterruptedException
Поток не может сбросить свой статус прерывания и нормально завершить вызов wait.
Аналогично, уведомления не могут быть утеряны из-за прерываний. Предположим, что набор s потоков находится в наборе ожиданий объекта m, и другой поток выполняет notify для m. Тогда либо:
-
по крайней мере один поток в s должен нормально завершить
wait, или -
все потоки в s должны выйти из
wait, сбросивInterruptedException
Обратите внимание, что если поток прерывается и пробуждается через notify, и этот поток завершается из wait, сбросив InterruptedException, то какой-то другой поток в наборе ожиданий должен получить уведомление.
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.
Модель памяти описывает, для заданной программы и трассировки выполнения этой программы, является ли эта трассировка законным выполнением программы. Модель памяти языка программирования 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 может переупорядочить код, или процессор. Кроме того, иерархия памяти архитектуры, на которой выполняется реализация виртуальной машины Java, может заставить код казаться переупорядоченным. В этой главе мы будем называть всё, что может переупорядочивать код, компилятором.
Другой пример неожиданных результатов можно увидеть в Таблице 17.4-C. Изначально, p == q и p.x == 0. Эта программа также неправильно синхронизирована; она записывает в общую память без принудительного упорядочения между этими записями.
Модель памяти определяет, какие значения могут быть прочитаны в любой точке программы. Действия каждого потока изолированно должны вести себя в соответствии с семантикой этого потока, за исключением того, что значения, видимые каждым чтением, определяются моделью памяти. Когда мы ссылаемся на это, мы говорим, что программа подчиняется семантике внутри потока. Семантика внутри потока — это семантика для однопоточных программ и позволяет полностью предсказать поведение потока на основе значений, увиденных действиями чтения внутри потока. Чтобы определить, являются ли действия потока t в выполнении законными, мы просто оцениваем реализацию потока t так, как оно выполнялось бы в однопоточном контексте, как определено в остальной части этого спецификации.
Каждый раз, когда оценка потока t генерирует межпотоковое действие, оно должно соответствовать межпотоковому действию a потока t, которое следует далее в порядке программы. Если a является чтением, то дальнейшая оценка t использует значение, увиденное a, как определено моделью памяти.
Этот раздел предоставляет спецификацию модели памяти языка программирования Java, за исключением вопросов, связанных с полями final, которые описаны в §17.5.
Модель памяти, описанная здесь, не основана фундаментально на объектно-ориентированной природе языка программирования Java. Для краткости и простоты в наших примерах мы часто демонстрируем фрагменты кода без определений классов или методов или явного разыменования. Большинство примеров состоят из двух или более потоков, содержащих инструкции с доступом к локальным переменным, общим глобальным переменным или полям экземпляра объекта. Мы обычно используем имена переменных, такие как r1 или r2, чтобы указать переменные, локальные для метода или потока. Такие переменные недоступны для других потоков.
Память, которая может быть общая для потоков, называется общей памятью или кучной памятью.
Все поля экземпляров, поля static и элементы массивов хранятся в кучной памяти. В этой главе мы используем термин переменная для обозначения как полей, так и элементов массивов.
Локальные переменные (§14.4), формальные параметры методов (§8.4.1) и параметры обработчиков исключений (§14.20) никогда не являются общими для потоков и не подвержены влиянию модели памяти.
Два доступа к (чтению или записи в) одной и той же переменной считаются конфликтующими, если хотя бы один из доступов является записью.
Межпотоковое действие — это действие, выполняемое одним потоком, которое может быть обнаружено или непосредственно повлиять на другой поток. Существует несколько видов межпотоковых действий, которые может выполнять программа:
-
Чтение (нормальное или не-volatile). Чтение переменной.
-
Запись (нормальная или не-volatile). Запись в переменную.
-
Действия синхронизации, которые включают:
-
Чтение volatile. Чтение volatile переменной.
-
Запись volatile. Запись volatile переменной.
-
Блокировка. Блокировка монитора.
-
Разблокировка. Разблокировка монитора.
-
Первое и последнее (синтетические) действия потока.
-
Действия, которые запускают поток или обнаруживают завершение потока (§17.4.4).
-
-
Внешние действия. Внешнее действие — это действие, которое может быть наблюдаемо вне выполнения и имеет результат, основанный на внешней среде выполнения.
-
Действия расхождения потоков (§17.4.9). Действие расхождения потоков выполняется только потоком, который находится в бесконечном цикле, в котором не выполняются никакие действия с памятью, синхронизацией или внешние действия. Если поток выполняет действие расхождения потоков, за ним последует бесконечное число действий расхождения потоков.
Действия расхождения потоков вводятся для моделирования того, как один поток может привести к остановке всех других потоков и не позволит им делать прогресс.
Этот стандарт касается только межпотоковых действий. Нам не нужно беспокоиться о внутрипотоковых действиях (например, сложение двух локальных переменных и сохранение результата в третьей локальной переменной). Как уже упоминалось, все потоки должны подчиняться правильным внутрипотоковым семантикам для Java-программ. Обычно мы будем кратко называть межпотоковые действия просто действиями.
Действие a описывается кортежем < t, k, v, u >, включающим:
-
t - поток, выполняющий действие
-
k - тип действия
-
v - переменная или монитор, участвующий в действии.
Для действий блокировки v — монитор, который блокируется; для действий разблокировки v — монитор, который разблокируется.
Если действие представляет собой (volatile или не-volatile) чтение, v — переменная, которая читается.
Если действие представляет собой (volatile или не-volatile) запись, v — переменная, которая записывается.
-
u - произвольный уникальный идентификатор действия
Кортеж внешнего действия содержит дополнительный компонент, который содержит результаты внешнего действия, как они воспринимаются потоком, выполняющим действие. Это может быть информация о успехе или неудаче действия, и любые значения, прочитанные действием.
Параметры внешнего действия (например, какие байты записываются в какой сокет) не являются частью кортежа внешнего действия. Эти параметры устанавливаются другими действиями в потоке и могут быть определены путем изучения внутрипотоковых семантик. Они не обсуждаются в модели памяти явно.
В бесконечных выполнениях не все внешние действия наблюдаемы. Бесконечные выполнения и наблюдаемые действия обсуждаются в §17.4.9.
Среди всех межпотоковых действий, выполняемых каждым потоком t, порядок программы потока t — это общий порядок, который отражает порядок, в котором эти действия были бы выполнены в соответствии с внутрипотоковой семантикой t.
Множество действий последовательно согласованно, если все действия происходят в общем порядке (порядке выполнения), который согласован с порядком программы, и, кроме того, каждое чтение r переменной v видит значение, записанное записью w в v, таком что:
-
w предшествует r в порядке выполнения, и
-
нет другой записи w' такой, что w предшествует w' и w' предшествует r в порядке выполнения.
Последовательная согласованность — это очень сильная гарантия видимости и упорядоченности в выполнении программы. В последовательно согласованном выполнении существует общий порядок всех отдельных действий (таких как чтения и записи), который согласуется с порядком программы, и каждое отдельное действие является атомарным и мгновенно видно для каждого потока.
Если в программе нет гонок данных, то все выполнения программы будут выглядеть последовательно согласованными.
Последовательная согласованность и/или отсутствие гонок данных всё ещё допускают ошибки, возникающие из групп операций, которые должны быть восприняты атомарно, но не являются таковыми.
Если мы будем использовать последовательную согласованность в качестве нашей модели памяти, то многие из обсуждавшихся оптимизаций компилятора и процессора будут незаконными. Например, в представленном следе в таблице 17.4-C, как только произошла запись 3 в p.x, последующие чтения этой позиции должны будут видеть это значение.
Каждое выполнение имеет порядок синхронизации. Порядок синхронизации — это общий порядок всех действий синхронизации в данном выполнении. Для каждого потока t порядок синхронизации действий синхронизации (§17.4.2) в t согласован с порядком выполнения программы (§17.4.3) потока t.
Действия синхронизации вызывают отношение synchronized-with на действиях, определяемое следующим образом:
-
Действие разблокировки монитора m synchronizes-with все последующие действия блокировки на m (где «последующее» определяется порядком синхронизации).
-
Запись в переменную v типа volatile (§8.3.1.4) synchronizes-with все последующие чтения v любым потоком (где «последующее» определяется порядком синхронизации).
-
Действие, которое запускает поток synchronizes-with первое действие в запущенном потоке.
-
Запись значения по умолчанию (ноль,
falseилиnull) в каждую переменную synchronizes-with первое действие в каждом потоке.Хотя может показаться немного странным, что значение по умолчанию записывается в переменную до выделения памяти для объекта, содержащего переменную, концептуально каждый объект создается в начале программы со значениями по умолчанию.
-
Конечное действие в потоке
T1synchronizes-with любое действие в другом потокеT2, который обнаруживает, чтоT1завершился.T2может достичь этого, вызвавT1.isAlive()илиT1.join(). -
Если поток
T1прерывает потокT2, прерывание отT1synchronizes-with любой момент, когда любой другой поток (включаяT2) определяет, чтоT2был прерван (путем выбросаInterruptedExceptionили вызоваThread.interruptedилиThread.isInterrupted).
Источник ребра synchronizes-with называется освобождением, а пункт назначения — приобретением.
Два действия могут быть упорядочены с помощью отношения happens-before. Если одно действие happens-before другое, то первое действие видно и упорядочено перед вторым.
Если у нас есть два действия x и y, мы записываем hb(x, y), чтобы указать, что x происходит перед y.
-
Если x и y являются действиями одного потока и x предшествует y в порядке программы, то hb(x, y).
-
Существует ребро happens-before от конца конструктора объекта к началу финализатора (§12.6) для этого объекта.
-
Если действие x синхронизируется с последующим действием 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 любых действий в запущенном потоке. -
Все действия в потоке happen-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. Разрешение чтения видеть более поздние записи может иногда приводить к неприемлемому поведению.
Выполнение 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).
Мы рассматриваем только корректные выполнения. Выполнение E = < P, A, po, so, W, V, sw, hb > является корректным, если следующие утверждения верны:
-
Каждое чтение видит запись в ту же переменную в выполнении.
Все чтения и записи переменных volatile являются действиями volatile. Для всех чтений r в A у нас есть W(r) в A и W(r).v = r.v. Переменная r.v является volatile тогда и только тогда, когда r является volatile чтением, и переменная w.v является volatile тогда и только тогда, когда w является volatile записью.
-
Порядок происходит-до является частичным порядком.
Порядок происходит-до задается транзитивным замыканием ребер синхронизируется-с и порядком программы. Он должен быть корректным частичным порядком: рефлексивным, транзитивным и антисимметричным.
-
Выполнение подчиняется внутренней согласованности нити.
Для каждой нити t, действия, выполненные нитью t в A, такие же, как были бы сгенерированы этой нитью в порядке программы в изоляции, где каждая запись w записывает значение V(w), при условии, что каждое чтение r видит значение V(W(r)). Значения, видимые каждым чтением, определяются моделью памяти. Указанный порядок программы должен отражать порядок программы, в котором действия выполнялись бы в соответствии с семантикой внутри-нити P.
-
Выполнение является последовательно-согласованным (§17.4.6).
-
Выполнение подчиняется согласованности порядка синхронизации.
Для всех 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. Формально:
-
Ci является подмножеством Ai
-
hbi|Ci = hb|Ci
-
soi|Ci = so|Ci
Значения, записанные записями в Ci, должны быть одинаковыми как в Ei, так и в E. Только чтения в Ci-1 должны видеть те же записи в Ei, что и в E. Формально:
-
Vi|Ci = V|Ci
-
Wi|Ci-1 = W|Ci-1
Все чтения в Ei, которые не входят в Ci-1, должны видеть записи, которые происходят раньше них. Каждое чтение r в Ci - Ci-1 должно видеть записи в Ci-1 как в Ei, так и в E, но может видеть другую запись в Ei по сравнению с той, которую оно видит в E. Формально:
-
Для любого чтения r в Ai - Ci-1, у нас есть hbi(Wi(r), r)
-
Для любого чтения r в (Ci - Ci-1), у нас есть Wi(r) в Ci-1 и W(r) в Ci-1
Учитывая множество достаточных ребер синхронизации для Ei, если существует пара «выпуск-приобретение», которая происходит раньше (§17.4.5) действия, которое вы фиксируете, то эта пара должна присутствовать во всех Ej, где j ≥ i. Формально:
-
Пусть sswi - это ребра swi, которые также находятся в транзитивном сокращении hbi, но не в po. Мы называем sswi достаточными ребрами синхронизации для Ei. Если sswi(x, y) и hbi(y, z) и z в Ci, то swj(x, y) для всех j ≥ i.
Если действие y зафиксировано, все внешние действия, которые происходят раньше y, также фиксируются.
-
Если 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 мы не можем выполнить ни одну запись раньше, потому что записи не могут произойти, пока чтения не увидят результат гонки данных.
Для программ, которые всегда завершаются в какой-то ограниченной конечный период времени, их поведение можно понять (неформально) просто в терминах их допустимых выполнений. Для программ, которые могут не завершаться за ограниченное время, возникают более тонкие вопросы.
Наблюдаемое поведение программы определяется конечными множествами внешних действий, которые может выполнять программа. Программа, которая, например, просто печатает "Привет" бесконечно, описывается множеством поведений, которые для любого неотрицательного целого числа i включают поведение печати "Привет" 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, но другие (внутренние) ограничения на то, как генерируются и выполняются внешние действия, могут накладывать такие ограничения.
Поля, объявленные 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 и обычное (не-final) 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 ссылок между потоками.
Пусть o — объект, а c — конструктор для o, в котором записывается final поле f. Действие freeze для final поля f объекта o происходит при завершении конструктора c, будь то нормальное завершение или аварийное.
Обратите внимание, что если один конструктор вызывает другой конструктор, и вызванный конструктор устанавливает final поле, действие freeze для 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, действие freeze 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 поля.
Чтение final поля объекта внутри потока, который строит этот объект, упорядочено относительно инициализации этого поля внутри конструктора обычными правилами happens-before. Если чтение происходит после того, как поле установлено в конструкторе, оно видит значение, присвоенное final полю, иначе — значение по умолчанию.
В некоторых случаях, таких как десериализация, системе может потребоваться изменить поля final объекта после его создания. Поля final могут быть изменены с помощью рефлексии и других механизмов, зависящих от реализации. Единственный шаблон, в котором это имеет разумную семантику, заключается в том, что объект создается, а затем поля final объекта обновляются. Объект не должен становиться видимым для других потоков, и поля final не должны читаться, пока не завершены все обновления полей final объекта. Фиксация поля final происходит как в конце конструктора, в котором устанавливается поле final, так и сразу после каждого изменения поля final с помощью рефлексии или другого специального механизма.
Даже тогда существует ряд осложнений. Если поле final инициализируется константным выражением (§15.28) в объявлении поля, изменения поля 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, обрабатываемых тем же исполнителем.
Обычно поле, которое является final и static, не может быть изменено. Однако, поля System.in, System.out и System.err являются защищенными от записи полями static final, которые по причинам совместимости должны быть изменены методами System.setIn, System.setOut и System.setErr. Мы называем эти поля защищенными от записи, чтобы отличить их от обычных полей final.
Компилятор должен обрабатывать эти поля иначе, чем другие поля final. Например, чтение обычного поля final «защищено» от синхронизации: барьер, связанный с блокировкой или чтением volatile, не должен влиять на то, какое значение читается из поля final. Поскольку значение защищенных от записи полей может быть изменено, события синхронизации должны влиять на них. Следовательно, семантика диктует, что эти поля должны обрабатываться как обычные поля, которые не могут быть изменены пользовательским кодом, если этот пользовательский код не находится в классе System.
Одно из соображений для реализации виртуальной машины 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();
}
}
Это подчеркивает, что байты не должны перезаписываться записями в смежные байты.
Для целей модели памяти языка программирования 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.