Содержание | Предыдущий | Следующий | ИндексСпецификация Виртуальной машины JavaTM


ГЛАВА 8

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


Эта глава детализирует низкоуровневые действия, которые могут использоваться, чтобы объяснить взаимодействие потоков виртуальной машины Java с совместно используемой основной памятью. Это было адаптировано с минимальными изменениями из Главы 17 первого выпуска Спецификации языка JavaTM, Джеймсом Гослингом, Биллом Джоем, и Гаем Стилом.


8.1 Терминология и Платформа

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

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

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

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

Использование или присваивается, работа является сильно связанным взаимодействием между механизмом выполнения потока и рабочей памятью потока. Блокировка или разблокировала работу, сильно связанное взаимодействие между механизмом выполнения потока и основной памятью. Но передача данных между основной памятью и рабочей памятью потока слабо связывается. Когда данные копируются от основной памяти до рабочей памяти, два действия должны произойти: операция чтения, выполняемая основной памятью, сопровождаемой некоторое время спустя соответствующей работой загрузки, выполняется рабочей памятью. Когда данные копируются от рабочей памяти до основной памяти, два действия должны произойти: работа хранилища, выполняемая рабочей памятью, сопровождаемой некоторое время спустя соответствующей операцией записи, выполняется основной памятью. Может быть некоторое время транспортировки между основной памятью и рабочей памятью, и время транспортировки может отличаться для каждой транзакции; таким образом операции, инициируемые потоком на различных переменных, могут быть просмотрены другим потоком как происходящий в различном порядке. Для каждой переменной, однако, операции в основной памяти от имени любого потока выполняются в том же самом порядке как соответствующие операции тем потоком. (Это объясняется более подробно позже.)

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

Вот подробные определения каждой из операций:

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


8.2 Порядок выполнения и Непротиворечивость

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

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

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

Большинство правил в следующих разделах далее ограничивает порядок, в котором имеют место определенные действия. Правило может утвердить, что одно действие должно предшествовать или следовать за некоторым другим действием. Отметьте, что это отношение является переходным: если действие, Необходимость предшествует действию B, и B, должно предшествовать C, то Необходимость предшествует C. Программист должен помнить, что эти правила являются единственными ограничениями на упорядочивание действий; если никакое правило или комбинация правил не подразумевают, что действие, Необходимость предшествует действию B, то реализация виртуальной машины Java свободна выполнить действие B перед действием A, или выполнить действие B одновременно с действием A. Эта свобода может быть ключом к хорошей производительности. Наоборот, реализация не обязана использовать в своих интересах все свободы, данные это.

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


8.3 Правила О Переменных

Позвольте T быть потоком и V быть переменной. Есть определенные ограничения на операции, выполняемые T относительно V:

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

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

Отметьте, что это последнее правило применяется только к действиям потоком на той же самой переменной. Однако, есть более строгое правило для volatile переменные (§8.7).


8.4 Неатомарная Обработка double и long Переменные

Если a double или long переменная не объявляется volatile, тогда в целях загрузки, хранилища, чтения, и операций записи это обрабатывается, как будто это были две переменные 32 битов каждый; везде, где правила требуют одной из этих операций, две таких операции выполняются, один для каждой 32-разрядной половины. Способ тот, в который 64 бита a double или long переменная кодируется в два 32-разрядных количества, и порядок операций на половинах переменных не определяются Спецификацией языка Java.

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

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


8.5 Правила О Блокировках

Позвольте T быть потоком и L быть блокировкой. Есть определенные ограничения на операции, выполняемые T относительно L:

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


8.6 Правила О Взаимодействии Блокировок и Переменных

Позвольте T быть любым потоком, позволять V быть любой переменной, и позволять L быть любой блокировкой. Есть определенные ограничения на операции, выполняемые T относительно V и L:


8.7 Правила для volatile Переменные

Если переменная объявляется энергозависимая, то дополнительные ограничения применяются к операциям каждого потока. Позвольте T быть потоком и позволять V и W быть энергозависимыми переменными.


8.8 Наделенные даром предвидения Операции Хранилища

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

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

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


8.9 Обсуждение

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

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

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


8.10 Примеров: Возможная Подкачка

Рассмотрите класс, у которого есть переменные класса a и b и методы hither и yon:

class Sample {
    int a = 1, b = 2;
    void hither() {
    	a = b;
    }
    void yon() 
    	b = a;
    }
}
Теперь предположите, что два потока создаются и что один поток вызывает hither в то время как другой поток вызывает yon. Каков необходимый набор действий и каковы ограничения упорядочивания?

Давайте рассматривать поток, который вызывает hither. Согласно правилам, этот поток должен выполнить использование b сопровождаемый присваиванием a. Это - пустой минимум, требуемый выполнить звонок в метод hither.

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

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

Ситуация для потока, который вызывает yon подобно, но с ролями a и b обмененный.

Полный набор операций может быть изображен следующим образом:





Здесь стрелка от действия к действию B указывает, что Необходимость предшествует B.

В каком порядок может операции основной памятью происходить? Единственное ограничение состоит в том, что это не возможно оба для записи a предшествовать чтению a и для записи b предшествовать чтению b, потому что стрелки причинной связи в схеме сформировали бы цикл так, чтобы действие должно было предшествовать себе, который не позволяется. Предполагая, что дополнительное хранилище и операции записи должны произойти, есть три возможных упорядочивания, в которых основная память могла бы законно выполнить свои операции. Позволить ha и hb будьте рабочими копиями a и b для hither поток, позволить ya и yb будьте рабочими копиями для yon поток, и позволял ma и mb будьте основными копиями в основной памяти. Первоначально ma=1 и mb=2. Затем три возможных упорядочивания операций и получающихся состояний следующие:

Таким образом конечный результат мог бы состоять в том что в основной памяти, b копируется в a, a копируется в b, или значения a и b подкачиваются; кроме того рабочие копии переменных могли бы или не могли бы согласиться. Было бы неправильно, конечно, предположить, что любой из этих результатов более вероятен чем другой. Это - одно место, в котором поведение программы обязательно зависимо от синхронизации.

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

Теперь предположите, что мы изменяем пример, чтобы использовать synchronized методы:

class SynchSample {
    int a = 1, b = 2;
    synchronized void hither() {
    	a = b;
    }
    synchronized void yon() 
    	b = a;
    }
}
Снова давайте рассматривать поток, который вызывает hither. Согласно правилам, этот поток должен выполнить работу блокировки (на экземпляре класса SynchSample на котором hither метод вызывают) перед телом метода hither выполняется. Это сопровождается использованием b и затем присваивание a. Наконец, разблокировать работа на том же самом экземпляре SynchSample должен быть выполнен после тела метода hither завершается. Это - пустой минимум, требуемый выполнить звонок в метод hither.

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

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

Ситуация для потока, который вызывает yon подобно, но с ролями a и b обмененный.

Полный набор операций может быть изображен следующим образом:





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

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


8.11 Примеров: не в порядке Записи

Этот пример подобен этому в предыдущем разделе, за исключением того, что один метод присваивается к обеим переменным, и другой метод читает обе переменные. Рассмотрите класс, у которого есть переменные класса a и b и методы to и fro:

class Simple {
    int a = 1, b = 2;
    void to() {
    	a = 3;
    	b = 4;
    }
    void fro() 
    	System.out.println("a= " + a + ", b=" + b);
    }
}
Теперь предположите, что два потока создаются и что один поток вызывает to в то время как другой поток вызывает fro. Каков необходимый набор действий и каковы ограничения упорядочивания?

Давайте рассматривать поток, который вызывает to. Согласно правилам, этот поток должен выполнить присваивание a сопровождаемый присваиванием b. Это - пустой минимум, требуемый выполнить звонок в метод to. Поскольку нет никакой синхронизации, это в опции реализации, сохранить ли присвоенные значения назад к основной памяти! Поэтому, поток, который вызывает fro может получить также 1 или 3 для значения a и независимо может получить также 2 или 4 для значения b.

Теперь предположите это to synchronized но fro не:

class SynchSimple {
    int a = 1, b = 2;
    synchronized void to() {
    	a = 3;
    	b = 4;
    }
    void fro() 
    	System.out.println("a= " + a + ", b=" + b);
    }
}
В этом случае метод to будет вынужден сохранить присвоенные значения назад к основной памяти перед разблокировать работой в конце метода. Метод fro должен, конечно, использовать a и b (в том порядке), и так должен загрузить значения для a и b от основной памяти.

Полный набор операций может быть изображен следующим образом:



Здесь стрелка от действия к действию B указывает, что Необходимость предшествует B.

В каком порядок может операции основной памятью происходить? Отметьте, что правила не требуют той записи a происходите перед записью b; и при этом они не требуют того чтения a происходите прежде, чем считано b. Кроме того, даже при том, что метод to синхронизируется, метод fro не синхронизируется, таким образом нет ничего, чтобы препятствовать тому, чтобы операции чтения произошли между блокировкой и разблокировали операции. (Дело в том, что объявление одного метода synchronized не делает себя, заставляют тот метод вести себя, как будто это было атомарным.)

В результате метод fro мог все еще получить также 1 или 3 для значения a и независимо мог получить также 2 или 4 для значения b. В частности fro мог бы наблюдать значение 1 для a и 4 для b. Таким образом, даже при том, что to делает присваивание a и затем присваивание b, операции записи к основной памяти, как может наблюдать другой поток, происходят как будто в противоположном порядке.

Наконец, предположите это to и fro оба synchronized:

class SynchSynchSimple {
    int a = 1, b = 2;
    synchronized void to() {
    	a = 3;
    	b = 4;
    }
    synchronized void fro() 
    	System.out.println("a= " + a + ", b=" + b);
    }
}
В этом случае, действия метода fro не может быть чередован с действиями метода to, и так fro напечатает любого"a=1, b=2"или"a=3, b=4".


8.12 Потоков

Потоки создаются и управляются классами Thread и ThreadGroup. Создание a Thread объект создает поток, и это - единственный способ создать поток. Когда поток создается, это еще не активно; это начинает работать когда start метод вызывают.


8.13 Блокировок и Синхронизация

Есть блокировка, связанная с каждым объектом. Язык программирования Java не обеспечивает способ выполнить отдельную блокировку и разблокировать операции; вместо этого, они неявно выполняются высокоуровневыми конструкциями, которые всегда располагают соединить такие операции правильно. (Виртуальная машина Java, однако, обеспечивает отдельный monitorenter и monitorexit инструкции, которые реализуют блокировку и разблокировали операции.)

synchronized оператор вычисляет ссылку на объект; это тогда пытается выполнить работу блокировки на том объекте и не продолжается далее, пока работа блокировки успешно не завершилась. (Работа блокировки может быть задержана, потому что правила о блокировках могут препятствовать тому, чтобы основная память участвовала, пока некоторый другой поток не готов выполнить один, или больше разблокировало операции.) После того, как работа блокировки была выполнена, тело synchronized оператор выполняется. Обычно, компилятор для языка программирования Java гарантирует что работа блокировки, реализованная monitorenter инструкцией, выполняемой до выполнения тела synchronized оператор является соответствующим разблокировать работой, реализованной monitorexit инструкцией всякий раз, когда synchronized оператор завершается, является ли завершение нормальным или резким.

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

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

Хотя компилятор для языка программирования Java обычно гарантирует структурированное использование блокировок (см. Раздел 7.14, "Синхронизацию"), нет никакого обеспечения, что весь код, представленный виртуальной машине Java, повинуется этому свойству. Реализации виртуальной машины Java разрешаются, но не требуются осуществлять оба из следующих двух правил, гарантирующих структурированную блокировку.

Позвольте T быть потоком и L быть блокировкой. Затем:

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

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

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


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

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

Ожидайте наборы используются методами wait, notify, и notifyAll из класса Object. Эти методы также взаимодействуют с механизмом планирования для потоков.

Метод wait должен быть вызван для объекта только, когда текущий поток (вызывают это T) уже заблокировал блокировку объекта. Предположите, что поток T фактически выполнил операции блокировки N на объекте, которые не были соответствующими, разблокировали операции на том же самом объекте. wait метод тогда добавляет текущий поток к ожидать набору для объекта, отключает текущий поток в целях планирования потоков, и выполняет N, разблокировали операции на объекте оставить блокировку на этом. Блокировки, заблокированные потоком T на объектах кроме одного T, должны ожидать на, не оставляются. Поток T тогда бездействует, пока одна из трех вещей не происходит:

Поток T тогда удаляется из ожидать набора и повторно включается для планирования потоков. Это тогда блокирует объект снова (который может включить конкуренцию в обычный способ с другими потоками); как только это получило контроль над блокировкой, это выполняет N - 1 дополнительная операция блокировки на том же самом объекте и затем возвращается из вызова wait метод. Таким образом, по возврату из wait метод, состояние блокировки объекта точно, как это было когда wait метод был вызван.

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

notifyAll метод должен быть вызван для объекта только, когда текущий поток уже заблокировал блокировку объекта, или IllegalMonitorStateException будет брошен. Каждый поток в ожидать наборе для объекта удаляется из ожидать набора и повторно включается для планирования потоков. (Те потоки не будут в состоянии продолжиться, пока текущий поток не оставляет блокировку объекта.)


Содержание | Предыдущий | Следующий | Индекс

Спецификация Виртуальной машины JavaTM
Авторское право © Sun Microsystems, Inc 1999 года. Все права защищены
Пожалуйста, отправьте любые комментарии или исправления к jvm@java.sun.com



Spec-Zone.ru - all specs in one place



free hit counter