Содержание | Предыдущая | Следующая


ГЛАВА 17

Потоки и замки

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

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

Оператор synchronized (§14.17) выполняет два специальных действия, которые имеют место только в многопоточных операциях: (1) после вычисления ссылки на объект, но перед выполнением его тела, блокирует замок, связанный с объектом, и (2) после выполнения тела заканчивает, или нормально, или преждевременно, он разблокирует тот же самый замок. Для удобства, метод может быть объявлен как synchronized; такой метод ведет себя как тело, которое содержится в операторе synchronized.

Методы wait (§ 20.1.6, § 20.1.7, § 20.1.8), notify (§ 20.1.9), и notifyAll (§ 20.1.10) класса Object поддерживают эффективную передачу управления от одного потока к другому. Более проще чем просто "прядение" (неоднократно блокируя и разблокируя объект, чтобы видеть, изменилось ли некоторое внутреннее состояние), которое потребляет вычислительную работу, поток может приостановить себя используя wait до такого времени как другой поток “проснется” используя notify. Это особенно свойственно для тех ситуаций, где потоки имеют связь создателя-потребителя (активно сотрудничающего для общей цели) быстрее, чем отношение взаимного исключения (пробующий избегать конфликтов при разделении общего ресурса).

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

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

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

Некоторые важные краткие следствия из правил:

17.1 Терминология и структура

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

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

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

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

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

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

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

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

17.2 Порядок выполнения

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

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

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

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

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

17.3 Правила относительно переменных

Пусть T - поток, а V - переменная. Существуют некоторые ограничения на действия выполняемые T относительно V:

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

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

Заметьте, что это последнее правило применяется только к действиям потока на ту же самую переменную. Однако, существует более строгое правило для volatile-переменных (§ 17.7).

17.4 Неатомарная обработка double и long

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

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

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

17.5 Правила относительно замков

Пусть T – поток, а L – замок. Существуют некоторые ограничения на действия выполняемые T относительно L:

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

17.6 Правила относительно взаимодействия замков и переменных

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

17.7 Правила для переменных

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

17.8 Опережающие действия сохранять

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

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

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

17.9 Обсуждение

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

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

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

17.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 основной памяти.

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

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

 

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

 

поток hither основная память поток yon

читать b читать a

 

загружать b загружать a

использовать b использовать a

присваивать a присваивать b

[ сохранять a ] [ сохранять b ]

 

[ записать a ] [ записать b ]

Здесь стрелка от действия А к действию B указывает, что A должно идти перед 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 будет выполнено. Это сопровождается действием использовать b и затем действием присваивать a. Наконец, действие разблокировать объект класса должно быть выполнено после выполнения тела метода hither. Это - достаточный минимум, необходимый, чтобы выполнить вызов метода hither.

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

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

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

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

 

поток hither основная память поток yon

 

 

блокировать класс SynchSample блокировать класс SynchSample

читать b читать a

загружать b загружать a

 

 

использовать b использовать a

присваивать a присваивать b

сохранить a сохранить b

 

 

записать a записать b

 

 

разблокировать класс SynchSample разблокировать класс SynchSample

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

В то время пока результат состояния зависит от времени, можно заметить, что два потока обязательно будут “согласовываться” со значениями a и b.

17.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 синхронизирован, но 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 должен, конечно, использовать а и b (в этом порядке) и должен загружать значения для а и b из основной памяти.

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

 

поток to основная память поток fro

читать a читать b

 

 

блокировать класс SynchSimple загружать a загружать b

присваивать a использовать a

присваивать b использовать b

сохранить a сохранить b печать

записать a записать b

разблокировать класс SynchSimple

 

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

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

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

Наконец, представьте, что to и fro синхронизированы:


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 будет печатать или "а=1, b=2" или "a=3, b=4".

17.12 Потоки

Потоки создаются и управляются встроенными классами Thread (§ 20.20) и ThreadGroup (§ 20.21). Создание объекта Thread создает поток, и это является единственным способом создания потока. Когда поток создан, он еще не активен; он начинает работать, когда вызывается его метод start (§ 20.20.14).

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

17.13 Замки и синхронизация

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

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

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

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

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

17.14 Наборы задержек и уведомления

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

Наборы задержек используются методами wait (§ 20.1.6, § 20.1.7, § 20.1.8), notify (§ 20.1.9), и notifyAll (§ 20.1.10) класса Object. Эти методы также взаимодействуют c механизмом планирования для потоков (§ 20.20).

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

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

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

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


Содержание | Предыдущая | Следующая