Глава 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.
- В противном случае имеет место следующая последовательность.
- Тред t добавляется в набор ожидания объекта m, и выполняет n разблокировок (unlock) на M.
- Тред 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 при этом прерывание будет в режиме ожидания.
- Тред t выполнить n блокировок на m.
- Если тред 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.
Спасибо за внимание!:)
Комментарии (20)
GerrAlt
18.05.2018 18:29+1А почему «тред»? Это устоявшийся перевод для Thread? Если нет, то мне кажется что что-то вроде «поток исполнения» выглядело бы более правильно и «по-русски», раз уж это перевод.
Serge78rus
18.05.2018 19:13+1Это устоявшийся жаргонизм. «Поток исполнения» — несколько длинновато, а просто «поток» иногда начинает путаться с потоками ввода-вывода.
werklop
18.05.2018 19:55+1Если делать перевод, то нужно избегать жаргонизмов, за исключением тех случаев, когда в русском языке(т.е. в том языке, на который делается перевод) нет подходящего по смыслу слова. В данном случае «поток» в полне приемлем и понятен, путаться с потоками ввода-вывода не приходится, т.к. имеется вполне конкретный контекст оригинальной статьи.
Serge78rus
19.05.2018 09:22Но ведь русский язык живой и развивается, и кому, как не нам формировать его в нашей профессиональной области. Согласен, в текущий момент слово «тред» несколько режет слух, но ситуация неоднозначности, сложившаяся с переводом stream, thread и fibre, возможно, в будущем приведет к корректировке устоявшихся русскоязычных терминов.
werklop
19.05.2018 13:45Еще раз повторяю — если в русском языке нет перевода, то необходимо внести слово в язык в виде неологизма. То, что используется сейчас, типа треда, это не что иное, как повторение за неким неучем, из-а которого все мы используем и говорим жаргонизмами
Serge78rus
19.05.2018 14:19Почему нет перевода?
- stream — поток
- thread — нить
- fibre — волокно
Что касается данного перевода, то выше уже высказана масса более существенных претензий, так что на такие мелочи, как жаргонизмы, тем более всем понятные, можно было бы и закрыть глаза.
netch80
19.05.2018 19:59Можно говорить «нить». Это традиционно, например, для книг по Unix.
Поддерживаю, что перевод как «поток» грубо диверсионно и должно быть исключено из практики.
1ennier
19.05.2018 11:41«объекты в нахоящиеся в общей памяти»
«временым разделение одного аппаратного процессора»
«значения могут быть увидены»
…
И это я еще под кат не заглядывал…
Timpo
19.05.2018 14:38На 20% я понял что это лучше не читать
«тело synchronized оператора выполнено полностью или в сокращенном варианте» что за сокращенный вариант оператора ?) если посмотрите в оригинал, узнаете)
«Метод synchronized (§8.4.3.6) автоматически выполняет захват» что за метод синхронайзд придуманный автором опять же узнаете в оригинале
Kolyuchkin
Качество перевода плохое… Такое ощущение, что перевод сделан программным переводчиком без последующего «облагораживания» знающим человеком.
Arx777 Автор
Ошибаетесь, делал сам и проверял преподаватель английского с опытом +30 лет. Но, спасибо за комментарий.:)
Kolyuchkin
Начнем с первого абзаца.
— зачем здесь запятая?1) Первое предложение перевода не согласовано (да и вообще не понятно);
2)
3) — запятая и «в находящиеся в», да и «независимо» пишется слитно;
4) В последнем предложении первого абзаца аналогичные ошибки.
И это только первый абзац, и только указаны синтаксические и орфографические ошибки, не считая ошибок тематических… В других абзацах ситуация аналогичная.
З.Ы. Английский Ваш может и хорош, но русский, как и владение темой, оставляют желать лучшего. Не все учителя английского знают программирование.
З.Ы.-2 Я Вам рекомендую почитать качественные переводы в области программирования для улучшения своих навыков.
sshikov
Нет, не ошибаемся. Может делали и сами — но качество плохое. Просто пример, один из многих:
>Поведение тредов особенно, когда синхронизация выполнена не корректно, может быть непонятно и не соответствовать ожиданиям.
Поведение особенно? Я не знаю, что у вас за преподаватель, но запятые в этом русском тексте точно никто не вычитывал. Ну и спеллчекер похоже тут тоже не применялся, а зря. Несогласованных слов в предложениях кучка. Это сильно портит впечатление, возможно если это исправить — текст уже был бы не так и плох, хотя глаз все равно постоянно натыкается на кальки с английского. Порядок слов, то, се. Это пока не перевод, а подстрочник.
Ну и еще — а вы думали, кому это вообще нужно? Этот документ — он не для начинающих. И он не учебник. Если кому-то хочется почитать про это на русском — то намного лучшей идеей был бы перевод скажем книги Java Concurrency in Practice, JCIP, если этого уже не сделали.
Arx777 Автор
По поводу орфографии частично уже поправил.
Кому это нужно? Меньше чем за сутки +700 просмотров, значит кому-то да интересно. Да и переводил для себя, в первую очередь, так как интересно лично мне.
Если когда-нибудь какое-либо издательство будет заинтересованно в переводе JCIP, то купит права, как на перевод, так и на издание и будет заниматься этим профессиональный переводчик, а за ним вычитывать редактор. Поскольку я не покупал права ни на перевод не на издание JCIP, то думаю, что данный вопрос закрыт.:)
sshikov
Я возможно повторюсь, но JLS это не учебник. Это справочник, причем по большей части — по весьма тонким и специфическим вопросам. И он по большей части ничего не объясняет, вы сами можете видеть, что и примеров тут небогато.
И еще он меняется с каждой версией языка.
Переводить такие документы — неблагодарное дело, это трудно, долго, и устаревает сразу.
Arx777 Автор
Я ж не заявлял, что это учебник:)
Учебников и так хватает. Но, переводил данную главу спецификации, ибо уяснить из первоисточника некоторые интересующие вопросы. И посчитал, что мб так же интересно людям. От коллег слышал, что некоторым из них читая сначала на английском, а потом на русском лучше осознается материал.
По поводу меняется с каждым выходом, для 8 и 9 версии именно этой главы идентичны, 7 и 8|9 отличается в нумерации пары табличек из разряда: в 7 версии нумерация шла 17.1,17.2 таблица в разделе 17.4, а в 8|9 версии это поправили на 17.4-A, 17.4-B для больший наглядности. Ну и есть расхождение в 2-ух местах в одно слово-определитель между 7 и 8|9 версиях. И в некоторых местах поправили название с строчной на прописную букву. На мой взгляд, если что-то кардинально изменится в этой главе спецификации — затронет сильно всю java и в этом случае уже многие официальные книги/статьи «устареют сразу».
Понятно, что переводить подобное не благодарное дело. Если писать статью для лайков/просмотров, то надо писать о «Hello World»-ах, но книг и статей на данную тему, на мой субъективный взгляд, уже достаточно на любых языках и в любых трактовках.:)
Inine
Уверен, что если вы просто скинете это в ворд, то увидите кучу ошибок.