Как стать автором
Обновить

The Java Language Specification. Chapter 17. Threads and Locks (Перевод. Часть 1)

Время на прочтение 8 мин
Количество просмотров 5.3K
Привет, Хабр! Представляю вашему вниманию перевод статьи «The Java Language Specification (Chapter 17. Threads and LocksОригинал.

Глава 17. Треды и блокировки (Chapter 17. Threads and Locks)

В то время как большая часть дискуссий в предыдущих главах касалась только поведения кода, исполняемого одновременно и как единое утверждение или выражение одновременно, т.е. в одном треде, JVM (Java virtual machine) может поддерживать одновременно несколько тредов исполнения. Эти треды независимо друг от друга используют код, который действует на значения и объекты, находящиеся в общей памяти (shared main memory). Треды могут поддерживаться за счет использования множества аппаратных процессоров, временным разделением одного аппаратного процессора или временным разделением нескольких аппаратных процессоров.

Треды представлены классом Thread. Единственный способ, каким пользователь может создать тред — это создать объект этого класса; каждый тред ассоциируется с каким-то объектом. Тред начнет свое исполнение, когда будет вызван метод start() на соответствующем Thread-объекте.
Поведение тредов, особенно когда синхронизация выполнена некорректно, может быть непонятно и не соответствовать ожиданиям. Эта глава описывает семантику многопоточного программирования; она содержит правила, согласно которым значения можно увидеть для чтения в общей памяти, которая обновляется множеством тредов. Так как спецификация аналогична Memory Models для различных архитектур, эта семантика известна как Memory Model языка программирования Java. Когда не будет возникать путаницы, мы просто будем называть эти правила "Memory Model".

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

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

Язык программирования Java представляет множество механизмов для взаимодействия между тредами. Самые основополагающие из них — это методы синхронизации (synchronization), которая осуществляется с использованием мониторов (monitors). Каждый объект в Java ассоциируется с монитором, который тред может захватить или отпустить (lock/unlock). Одновременно, только один тред может держать монитор. Любые другие треды при попытке захватить этот монитор блокируются, пока они не смогут захватить его. Тред t может блокировать конкретный монитор множество раз; когда монитор отпускается (unlock), отменяется эффект одной операции блокировки (lock).

Оператор synchronized (§14.19) вычисляет ссылку на объект, а потом он пытается выполнить захват (lock) монитора этого объекта и дальше ничего не происходит, пока захват не выполнен успешно. После успешного захвата (lock) выполняется тело synchronized оператора. Если тело synchronized оператора выполнено полностью или в сокращенном варианте, то этот монитор автоматически отпускается (unlock).

Синхронизированный метод (§8.4.3.6) автоматически выполняет захват (lock) при вызове, его тело не исполняется, пока захват (lock) успешно не выполнен. Если мы имеем дело с методом экземпляра, тогда он захватывает монитор, связанный с экземпляром, для которого был вызван (то есть объектом, который будет известен как this в течение выполнения тела метода). Если метод статический (static), он захватывает монитор, связанный с объектом Class, который представляет класс, в котором определен метод. Если выполнение тела метода завершено полностью или в сокращенном варианте, этот монитор автоматически отпускается.
Язык программирования Java не предотвращает и не требует определения взаимоблокировки (deadlock) условий. Программы, где треды держат (прямо или косвенно) захват на множестве объектов, должны использовать обычные приемы для избежания взаимоблокировки. Создавайте высокоуровневые блокирующиеся примитивы, у которых не бывает взаимоблокировок, если необходимо.

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

17.2 Набор ожиданий и уведомления (17.2. Wait Sets and Notification)

Каждый объект, в дополнение к тому, что имеет ассоциацию с монитором, так же связан с набором ожиданий (Wait Sets). Набор ожиданий — представляет собой набор тредов.
Когда объект впервые создается, его набор ожиданий пуст. Элементарные действия, которые добавляют или удаляют треды в/из набор ожиданий атомарны. Набор ожиданий управляется исключительно через методы Object.wait, Object.notify, and Object.notifyAll.

На манипуляции с набором ожиданий так же могут повлиять статическое прерывание треда и методов класса Thread связанные с прерыванием (interruption). Кроме того методы класса Thread для sleeping и joining других тредов имеют свойства, полученные от свойств действий методов wait and notification.

17.2.1. Ожидание (17.2.1. Wait)

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

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

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

  • Если n ноль (т.е. тред t еще не захватил блокировку (lock) на целевой m-объект), тогда будет выброшено исключение IllegalMonitorStateException.
  • Если этот wait с заданой временной сигнатурой nanosecs-аргумент не в диапозоне 0-999999 или millisecs-аргумент задан негативным числом,, тогда будет выброшено исключение IllegalArgumentException
  • Если тред t прерывается, тогда будет выброшено исключение InterruptedException и состояние прерывания (interruption status) t устанавливается в false.
  • В противном случае имеет место следующая последовательность.
    1. Тред t добавляется в набор ожидания объекта m, и выполняет n разблокировок (unlock) на M.
    2. Тред t не выполняет больше никаких инструкций, пока не будет удален из набора ожиданий объекта m. Тред может быть удален из набора ожидания по любой из следующих причин и будет восстановлен когда-нибудь позже:

      • Действие notify было выполнено на m, в котором t выбран для удаления из набора ожиданий.
      • Действие notifyAll выполнено на m.
      • Действие interrupt выполнено на t.
      • Если wait с заданой временной сигнатурой, внутренние действие удаляющие t из набора ожиданий m, которое происходит после, по крайней мере millisecs плюс nanosecs после начала этого действия ожидания.
      • Внутреннее действие путем реализации. Реализация разрешена, хотя и не желательна, чтобы выполнить «ложные активации (spurious wake-ups)», то есть удалить тред из набора ожиданий и таким образом позволить возобновить действия без дополнительных инструкций для этого.
        Обратите внимание, что это положение требует практики кодирования на Java, при использовании wait только внутри циклов, которые заканчиваются только по логическом условии, что тред удерживает блокировку.

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

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

17.2.2. Уведомление (17.2.2. Notification)
Уведомление (notification) происходит при вызове метода notify and notifyAll.
Давайте представим, что тред t будет использовать любой из этих методов на объекте m, и пусть n будет число захвата блокировок на t по m, которому не соответствовали количество выполнения действий отпуска монитора (unlock).
Произойдет одно из следующих действий:

  • Если n равно нулю, то будет брошено IllegalMonitorStateException.
    Это случай, когда тред t уже не обладает блокировкой для целевого m-объекта.
  • Если n больше нуля и это notify действие, тогда, если набор ожиданий m не пуст, выбирается тред u являющийся членом текущего набора ожиданий m и его удаляют из набора ожиданий.
    Нет гарантии, какой тред из набора ожиданий будет выбран. Удаление треда u из набора ожиданий возобновляет u в wait-действие. Заметьте, однако, что действие захвата u, при возобновление, будет осуществляться спустя некоторое время после того как t полностью разблокирует монитор для m.
  • Если n больше нуля и выполняется notifyAll действие, тогда все треды удаляются из набора ожиданий m и таким образом возобновляются.
    Заметте, однако, что, одновременно, только один из них захватит требуемый монитор вовремя возобновления wait.

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

Дополнительно, если существует какой-то объект m чей набор ожиданий содержит u, тогда u удаляется из набора ожиданий m. Это включает u для возобновления в wait-действие, в этом случае, после повторного захвата монитора m будет брошено исключение InterruptedException.
Вызовы Thread.isInterrupted могут определить статусы прерывания тредов. Статический метод Thread.interrupted может быть вызван в треде для наблюдения и очистки его собственного статуса прерывания.

17.2.4. Взаимодействие Ожиданий, Уведомления, and Прерывания (17.2.4. Interactions of Waits, Notification, and Interruption)

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

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

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

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

  • Как минимум один тред и s должен вернуться нормально и ожидания, или
  • все треды из s должны выйти из и выбросить InterruptedException

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

17.3. Спать и Уступать (17.3. Sleep and Yield)

Thread.sleep переводит работающий тред в спящий режим (временное прекращение выполнения) на определенный срок в зависимости от точности таймеров (system timers) и планировщиков системы (schedulers). Тред не теряет контроль над мониторами и его действие возобновляется в зависимости от планирования и доступности процессоров на которых можно выполнять треды.
Важно упомянуть, что ни 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.

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

Часть 2) Memory Model;
Часть 3) Семантика final полей; Word Tearing на некоторых процессорах (х32); не атомарную поддержку double и long.

Спасибо за внимание!:)
Теги:
Хабы:
+2
Комментарии 20
Комментарии Комментарии 20

Публикации

Истории

Работа

Java разработчик
359 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн