Кодекс старика, посмотри на него и встань на колени!

Java

Вот почему 99-я оригинальная статья

Привет, я брат, почему.

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

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

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

А у меня, как у новичка, нет чувства сопричастности, но я чувствую, что то, что говорят воротилы, вполне резонно, и стремлюсь к этому.

В общем, весь мой процесс выглядит так:

Однако, когда они говорили и говорили о «Параллельном программировании на Java на практике», я сразу же поддержал их.

Я прочитал эту книгу, и она прямо рядом со мной, так что я наконец могу поговорить о ней.

При ближайшем рассмотрении речь идет о разделе 16.1.4 книги:

Изображений больше нет, и я даже не понимаю смысла слова "с помощью синхронизации".

Поэтому я зашел в этот раздел и прочитал его.

Так как этот раздел не длинный, и нет никакой другой предыстории, кроме базовых знаний об отношениях Happens-Before, я сделал скриншот этого раздела для всеобщего обозрения:

Как, как вы себя чувствуете после просмотра?

У вас даже не хватает терпения прочитать это, ощущение тумана?

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

Итак, после прочтения ощущение такое:

найти исходный код

Но не паникуйте, пример в статье — FutureTask, одна из основ параллельного программирования, я с ней знаком.

Поэтому я решил посмотреть исходный код, но не нашел упомянутого в книге метода innerSet или innerGet:

Так как вот исходный код JDK 8, а эта книга вышла в феврале 2012 года:

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

По сравнению с графиком выпуска этой версии JDK, если это исходный код, то это также исходный код до JDK 8:

Конечно же, один большой парень сказал мне, что исходный код в JDK 6 написан так:

Но я не думаю, что польза от изучения JDK 6 очень велика. (в основном потому что мне лень скачивать)

Итак, я все же нашел небольшую подсказку в исходном коде JDK 8.

Наконец-то поняли, что такое "с помощью синхронизации".

И я должен восхищаться кодом старика Дуга Ли, он действительно замечательный.

Что именно "с помощью синхронизации"? И слушайте меня подробно.

Фонд

Для плавного продвижения статьи необходимо подготовить базовые знания, то есть отношение «Происходит-до».

Формальным предложением отношения Happens-Before является спецификация jsr 133:

http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf

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

http://ifeve.com/jsr133/

Здесь есть формальное описание известной связи Happens-Before.Все оригинальные китайские переводы, которые вы видели, находятся здесь:

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

Поэтому я решил встать на плечи больших парней и поместить определения этой части в три книги «Углубленное понимание виртуальной машины Java (третье издание)», «Практика параллельного программирования на Java» и «Искусство Параллельное программирование на Java». Несите с описанием, давайте сравним.

Если вы знакомы с этим правилом, вы можете пропустить этот раздел.

погнали.

Первый — «Углубленное понимание виртуальной машины Java (третье издание)»:

  • Правило порядка программы: внутри потока, в порядке потока управления, операции, написанные впереди, выполняются до операций, написанных сзади. Обратите внимание, что мы говорим о порядке потока управления, а не о порядке кода программы, потому что рассматриваются структуры ветвления, циклов и т. д.
  • Правило блокировки монитора: сначала выполняется операция разблокировки перед последующей блокировкой того же замка. операция блокировки. Здесь необходимо подчеркнуть, что «тот же замок» и «позже» относятся к временной последовательности.
  • Правило для изменчивых переменных: операция записи в изменчивую переменную происходит сначала перед операцией чтения переменной, и «позже» здесь также относится к временной последовательности.
  • Правило запуска потока: метод start() объекта Thread предшествует каждому действию этого потока.
  • Правило завершения потока: все операции в потоке выполняются первыми при обнаружении завершения этого потока.Мы можем определить, завершается ли метод Thread::join(), возвращаемое значение Thread::isAlive() и т. д. прекращено исполнение.
  • Правило прерывания потока: вызов метода прерывания() потока происходит первым, когда код прерванного потока обнаруживает возникновение события прерывания и может определить, есть ли прерывание с помощью метода Thread:interrupted().
  • Правило финализатора: инициализация объекта (конец выполнения конструктора) происходит сначала в начале его метода finalize().
  • Транзитивность: если операция A происходит раньше операции B, а операция B происходит раньше операции C, то можно сделать вывод, что операция A происходит раньше операции C.

Далее идет «Практика параллельного программирования на Java»:

  • Правило порядка выполнения программы: если операция A предшествует операции B в программе, то операция A будет выполняться перед операцией B в потоке.
  • Правила блокировки монитора. Операция разблокировки блокировки монитора должна выполняться до операции блокировки того же замка монитора.
  • Правила для изменчивых переменных: запись в изменчивую переменную должна выполняться до чтения переменной.
  • Правила запуска потока: вызов Thread.Start в потоке должен выполняться до того, как в этом потоке будут выполнены какие-либо операции.
  • Правило завершения потока: любая операция в потоке должна быть выполнена до того, как другие потоки обнаружат, что поток завершился, или успешно завершится из Thread.join, или вернет false при вызове Thread.isAlive.
  • Правила прерывания: Когда поток вызывает прерывание в другом потоке, он должен сделать это до того, как прерванный поток обнаружит вызов прерывания (либо выбрасывая InterruptedException, либо вызывая isInterrupted и interrupted).
  • Правило финализатора: конструктор объекта должен завершиться до запуска финализатора объекта.
  • Транзитивность: если операция А выполняется до операции В, а операция В выполняется до операции С, то операция А должна выполняться до операции С.

«Искусство параллельного программирования на Java», в котором автор добавляет квалификатор «правила «происходит до того, как», тесно связанные с программистами, заключаются в следующем»:

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

То есть: правила запуска потока, правила завершения потока, правила прерывания и правила завершения объекта на самом деле безразличны к разработке.В этих правилах у нас нет места для проблем.

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

По сути, это то же самое, только немного другое описание.

Кроме того, я думаю, что мне нужно добавить пункт, который я считаю очень важным, а именно очень важное слово «действие», которое встречается во многих местах в оригинальной статье:

Так что же такое действие?

Для этого немного расплывчатого определения в пятом пункте в начале статьи упоминается конкретное значение:

In this section we define in more detail some of the informal concepts we have presented.
В этом разделе мы более подробно определим некоторые из предложенных нами неформальных понятий.

Подробно описаны семь концепций в статье, а именно:

  • Shared variables/Heap memory
  • Inter-thread Actions
  • Program Order
  • Intra-thread semantics
  • Synchronization Actions
  • Synchronization Order
  • Happens-Before and Synchronizes-With Edges

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

Действия между потоками, действия внутри потока, действия синхронизации.

Блокировка, разблокировка, чтение и запись в volatile-переменные, запуск потока и проверка завершения потока — все это синхронные действия.

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

Кроме того, если вы посмотрите на подчеркнутое место в Inter-thread Actions, описание почти такое же, как и у синхронных действий. Я понимаю, что на самом деле большинство действий между потоками — это синхронные действия.

Итак, вы переходите к книге под названием «Углубленное понимание Java Virtual Machine HotSpot». Описание «происходит до» в этой книге немного отличается. В начале добавлено уточнение: «все синхронные действия...»:

1) Последовательность кода всех действий синхронизации (блокировка, разблокировка, чтение и запись volatile переменных, запуск потока, завершение потока) согласуется с последовательностью выполнения.Последовательность кода действий синхронизации также называется последовательностью синхронизации.
1.1) Для одного и того же монитора в синхронном действии разблокировка происходит перед блокировкой.
1.2) Та же самая операция записи энергозависимой переменной происходит перед операцией чтения.
1.3) Операция запуска потока является первой операцией потока, и никакие операции, предшествующие ей, выполняться не могут.
1.4) Когда поток T2 обнаруживает, что поток T1 завершен или подключен к T1, последняя операция T1 должна быть до T2. все операции.
1.5) Если поток T1 прерывает поток T2, то точка прерывания T1 находится перед любым потоком, который определяет, что T2 прерван. работать.
Операции записи значения по умолчанию в переменную должна предшествовать первая операция потока, первой должна быть завершена операция завершения инициализации объекта в первой операции метода finalize().
2) Если а происходит раньше b, а b происходит раньше c, то можно определить, что a происходит раньше c.
3) Операция записи volatile предшествует операции чтения volatile.

Первоначально я также хотел процитировать описание «случается-до» в «Java Programming Thought».

В итоге пролистал параллелограммную часть книги и оказалось:

Нет, да, пишите!

Что ж, возможно, эта божественная книга была написана до того, как в 2004 году был выпущен jsr133?

В итоге его англоязычная версия вышла в 2006 году, то есть автор намеренно её не писал, он лишь упомянул «Java Concurreny in Practice» в главе 21.11.1:

А «Параллелизм на Java на практике» — это «Практика параллельного программирования на Java», о которой мы упоминали ранее.

Как книга с такой высокой репутацией в мире Java, немного прискорбно, что раньше не упоминалось.

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

Кроме того, интересное место это:

В «Углубленном понимании виртуальной машины Java (третье издание)» «Монитор» переводится как «монитор», а два других перевода - «монитор».

Так что же такое «менеджер»?

Черт, это то же самое.

Синхронизированный в Java является реализацией монитора.

FutureTask in JDK 8

Впереди столько предзнаменований, не стоит забывать, чем я в основном хочу поделиться в этой статье, верно?

То есть приложение «с синхронизацией» в FutureTask.

Вот скриншот исходного кода FutureTask в JDK 8, с акцентом на две части, которые я выделил.

  • состояние украшено volatile.
  • Комментарий, который следует за переменной результата.

Сосредоточьтесь на этой заметке:

non-volatile, protected by state reads/writes

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

Возьмем один из самых простых и распространенных сценариев приложения: основная линия отправляет задачи в пул потоков через submit, а возвращаемое значение — FutureTask:

Что вы будете делать дальше?

Вызываете ли вы метод get FutureTask в основном потоке, чтобы получить возвращаемое значение этой задачи?

Текущая ситуация такова: поток в пуле потоков записывает результат, а основной поток вызывает метод get для чтения результата?

В этом сценарии должны ли наши обычные операции добавлять переменную к результату, чтобы обеспечить видимость?

Так почему же здесь не добавлен volatile?

Ты сначала сам себя шлепни.

Далее все, что будет описано, вращается вокруг этой темы.

Иди, иди.

Прежде всего, глядя на весь мир, есть только два места для операции записи переменной результата:

set и setException, и логика и принципы этих двух мест на самом деле одинаковы. Поэтому я анализирую только метод set.

Далее смотрим на операцию чтения переменной результата, там есть только это место, которое является методом get:

Следует отметитьjava.util.concurrent.FutureTask#get(long, java.util.concurrent.TimeUnit)Принцип метода такой же, как и у метода get, поэтому нет необходимости слишком много интерпретировать его.

Поэтому мы сосредоточили наше внимание на этих трех методах:

Разве метод get не вызывает метод отчета? Давайте объединим эти два метода:

Что-то здесь не так?

Далее, нас на самом деле заботит только то, когда возвращается результат, остальные отвлекают меня, поэтому мы превращаем полученное выше в псевдокод:

Когда s является НОРМАЛЬНЫМ, возвращайте результат, этот псевдокод не является неправильным, верно?

Далее, давайте снова посмотрим на метод set:

Значение второй строки состоит в том, чтобы использовать операцию CAS для изменения состояния с NEW на COMPLETING и ввести сегмент кода if после успешного выполнения CAS.

Затем после третьей строки кода, т.е.outcome=vПосле этого состояние меняется на НОРМАЛЬНОЕ.

На самом деле, вы видите, от НОВОГО до НОРМАЛЬНОГО, состояние ЗАВЕРШЕНИЕ посередине, по сути, можно сказать, что оно быстротечно.

Даже, кажется бесполезным?

Итак, чтобы рассуждения шли гладко, я решил использовать метод контрдоказательств, предполагая, что нам не нужно это состояние ЗАВЕРШЕНИЯ, тогда наш метод set становится таким:

После упрощения это псевдокод окончательного набора:

Итак, мы собрали псевдокод для get/set:

На данный момент, наконец, все предзнаменования были завершены.

Добро пожаловать на сеанс расшифровки.

Прежде всего, если в месте, обозначенном ④, указано НОРМАЛЬНОЕ, это означает, что место, обозначенное ③, должно быть выполнено.

Зачем?

Поскольку s модифицируется volatile, в соответствии с отношением «происходит до»:

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

Таким образом, мы можем заключить, что код с меткой ③ выполняется раньше кода с меткой ④.

И согласно правилам порядка программы, а именно:

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

Можно сделать вывод, что ② происходит до того, как ③ произойдет, прежде чем ④ произойдет, прежде чем ⑤

Согласно транзитивному правилу, это:

Если операция А происходит раньше операции В, а операция В происходит раньше операции С, то можно сделать вывод, что операция А происходит раньше операции С.

Можно сделать вывод, что ② происходит раньше ⑤.

И ② — запись в переменную результата, а ⑤ — чтение переменной результата.

Хотя записываемая и читаемая переменная не добавляет volatile, она завершает операцию синхронизации через переменную s, измененную volatile, и отношение «происходит до» переменной s.

То есть: напишите, прежде чем читать.

Это "с помощью синхронизации".

Вы попробовали немного вкуса?

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

Оглядываясь назад на псевдокод метода set, я ничего не сказал о месте, помеченном ①.

Хотя место, помеченное ①, и место, помеченное ③, являются операциями с volatile-переменными, они не потокобезопасны.Можем ли мы с этим согласиться?

Итак, здесь мы должны использовать CAS для обеспечения потокобезопасности.

Итак, программа становится такой:

Таким образом решается проблема безопасности потоков. Но последовали другие проблемы.

Первая проблема в том, что изменился смысл программы:

От «после завершения назначения результата s становится НОРМАЛЬНЫМ» до «назначение начинается после того, как s становится НОРМАЛЬНЫМ».

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

В чем проблема?

То есть стратегия «с синхронизацией» на исходе не работает.

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

Из отношения «происходит до» мы можем сделать только вывод:

② происходит до того, как ④ происходит до ⑤, не имеет ничего общего с ③.

Итак, мы не можем получить ③ до того, как ⑤, поэтому мы не можем использовать синхронизацию.

В это время, что, если мы столкнемся с этим?

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

Но Дуг Ли, в конце концов, это Дуг Ли, добавление volatile настолько мало, что старик собирается «использовать синхронизацию».

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

protected void set(V v) {
    if (s==NEW) {
        outcome = v;
        s=NORMAL;
    }
}

Итак, можем ли мы сделать это так:

protected void set(V v) {
    if (s==NEW) {
        s=COMPLETING;
        outcome = v;
        s=NORMAL;
    }
}

COMPLETING также является записью в переменную s, чтобы результат можно было снова «синхронизировать».

Оптимизация с помощью CAS выглядит следующим образом:

protected void set(V v) {
    if (compareAndSet(s, NEW, COMPLETING)){
        outcome = v;
        s=NORMAL;
    }
}

Введение мимолетного состояния ЗАВЕРШЕНИЯ может сделать переменную результата не добавляющей изменчивости, а также может установить отношение «происходит до», которое может достичь цели «с помощью синхронизации».

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

Я должен сказать, старик этот код:

Это действительно "дерзко", я не могу учиться, я не могу учиться.

Кроме того, я еще раньше писал статью о FutureTask, описывая еще один баг:

Ошибка, написанная Дугом Ли в сумке J.U.C, снова была обнаружена пользователями сети.

Упоминается в этой статье:

Старик сказал, что "намеренно так написал", есть ли за этим подоплека "с помощью синхронизации"?

Не знаю, но у меня, кажется, ощущение "связи со сном".

Ну, эта статья размещена здесь.

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

до свидания.

Последнее слово

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

Спасибо за прочтение, настаиваю на оригинальности, очень приветствую и благодарю за внимание.