JSR 133 (Java Memory Model) FAQ (перевод)

    Добрый день.
    В рамках набора на курс «Multicore programming in Java» я делаю серию переводов классических статей по многопоточности в Java. Всякое изучение многопоточности должно начинаться с введения в модель памяти Java (New JMM), основным источником от авторов модели является «The Java Memory Model» home page, где для старта предлагается ознакомится с JSR 133 (Java Memory Model) FAQ. Вот с перевода этой статьи я и решил начать серию.
    Я позволил себе несколько вставок «от себя», которые, по моему мнению, проясняют ситуацию.
    Я являюсь специалистом по Java и многопоточности, а не филологом или переводчиком, посему допускаю определенные вольности или переформулировки при переводе. В случае, если Вы предложите лучший вариант — с удовольствием сделаю правку.
    Этот статья также подходит в качестве учебного материала к лекции «Лекция #5.2: JMM (volatile, final, synchronized)».

    Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).

    Ну и да, приходите учиться ко мне!


    JSR 133 (Java Memory Model) FAQ


    Jeremy Manson и Brian Goetz, февраль 2004

    Содержание:
    Что такое модель памяти, в конце концов?
    Другие языки, такие как C++, имеют модель памяти?
    Что такое JSR 133?
    Что подразумевается под «переупорядочением» (reordering)?
    Что было не так со старой моделью памяти?
    Что вы подразумеваете под «некорректно синхронизированы»?
    Что делает синхронизация?
    Как может случиться, что финальная поля меняют значения?
    How do final fields work under the new JMM?
    Что делает volatile?
    Решила ли новая модель памяти «double-checked locking» проблему?
    Что если я пишу виртуальную машину?
    Почему я должен беспокоиться?


    Что такое модель памяти, в конце концов?


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

    На уровне процессора, модель памяти определяет необходимые и достаточные условия для гарантии того, что записи в память другими процессорами будут видны текущему процессору, и записи текущего процессора будут видимы другими процессорами. Некоторые процессоры демонстрируют сильную модель памяти, где все процессоры всегда видят точно одинаковые значения для любой заданной ячейки памяти. Другие процессоры демонстрируют более слабую модель памяти, где специальные инструкции, называемые барьерами памяти, требуются для «сброса» (flush) или объявления недействительными (invalidate) данных в локальном кэше процессора, с целью сделать записи данного процессора видимыми для других или увидеть записи, сделанные другими процессорами. Эти барьеры памяти, как правило, выполняются при захвате (lock) и освобождении (unlock) блокировки; они невидимы для программистов на языках высокого уровня.

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

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

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

    Простой пример этого можно увидеть в следующем фрагменте кода:
    class Reordering {
        int x = 0, y = 0;
        public void writer() {
            x = 1;
            y = 2;
        }
        public void reader() {
            int r1 = y;
            int r2 = x;
        }
    }
    


    Давайте предположим, что этот код выполняется в двух потоках одновременно и чтение 'у' возвращает значение 2. Поскольку эта запись расположена после записи в 'х', программист может предположить, что чтение 'х' должно вернуть значение 1. Тем не менее, запись в 'x' и 'y', возможно, были переупорядочены. Если это имело место, то могла произойти запись в 'у', затем чтение обеих переменных, и только потом запись в 'х'.Результатом будет то, что r1 имеет значение 2, а r2 имеет значение 0.
    Комментарий переводчика
    Предполагается, что у одного и того же объекта метод reader() и метод writer() «почти одновременно» вызываются из различных потоков.

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

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

    Другие языки, такие как C + +, имеют модель памяти?


    Большинство других языков программирования, таких как C и C++, не были разработаны с прямой поддержкой многопоточности. Защитные меры, которые эти языки предлагают против различных видов переупорядочения, происходящих в компиляторах и просессорах во многом зависят от гарантий, предлагаемых используемыми библиотеками распараллеливания (например, pthreads), используемым компилятором и платформой, на которой запускается код.
    Комментарий переводчика
    Java задала тренд введения модели памяти в спецификации языка и в последнем стандарте C++11 уже есть модель памяти (глава 1.7 «The C++ memory model»). Кажется она есть уже и у C11.


    Что такое JSR 133?


    С 1997 года были обнаружены несколько серьезных недостатков в модели памяти Java, которая определена в главе 17 спецификации языка. Эти недостатки допускали шокирующее поведение (например, позволялось изменение значения final-поля) и препятствовали компилятору в использовании типичных оптимизаций.
    Комментарий переводчика
    Старая модель памяти (Old JMM, до Java 5) была описана исключительно в Главе 17 «Chapter 17. Threads and Locks» спецификации языка.
    Новая модель памяти (New JMM, начиная с Java 5) описана как в Главе 17 «Chapter 17. Threads and Locks» спецификации языка так и расширенно в отдельном документе (JSR-133).
    New JMM
    Old JMM:


    Модель памяти Java была амбициозным проектом; впервые спецификация языка программирования попыталась включить модель памяти, которая может обеспечить согласованную семантику для параллелизма среди различных процессорных архитектур. К сожалению, определить модель памяти, которая является и согласованной и интуитивной оказалось гораздо труднее, чем ожидалось. JSR 133 определяет новую модель памяти для Java, которая исправляет недостатки предыдущей модели. Для того чтобы сделать это, семантика final и volatile была изменена.

    Полное описание семантики доступно по ссылке http://www.cs.umd.edu/users/pugh/java/memoryModel, но формальное описание не для робких. Удивляет и отрезвляет когда узнаешь, насколько сложным является такое простое понятие, как синхронизации на самом деле. К счастью, вам не нужно понимать все детали формальной семантики — целью JSR 133 было создать набор правил, который обеспечивает интуитивное понимание того, как работают volatile, synchronized и final.
    Комментарий переводчика
    Автором дана ссылка на «домашнюю страницу» Java Memory Model — основной источник информации в интернете.

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

    Цели JSR 133 включают в себя:
    • Сохранение существующих гарантий безопасности, таких как безопасность типов (type safety), а также усиление других. Например, значения переменных не могут появиться «из воздуха»: каждое значение переменной наблюдаемое каким-либо одним потоком должно быть значением, которое было записано каким-то другим потоком.
    • Семантика корректно синхронизированных программ должна быть настолько простой и интуитивно понятной, насколько это возможно.
    • Семантика не полно или некорректно синхронизированных программ должна быть определена таким образом, чтобы потенциальные угрозы безопасности были минимизированы.
    • Программисты должны иметь возможность уверенно рассуждать о том, как многопоточные программы взаимодействуют с памятью.
    • Должно быть возможна разработка корректных и высокопроизводительных JVM поверх широкого диапазона популярных аппаратных архитектур.
    • Должны быть обеспечена новая гарантия безопасности инициализации. Если объект правильно построен (не было «утечек» ссылок на него во время конструирования), то все потоки, которые видят ссылку на этот объект также без необходимости синхронизации будут видеть значения final-полей, которые были установлены в конструкторе.
    • Должно быть обеспечено минимальное влияние на существующий код.


    Что подразумевается под «переупорядочением» (reordering)?


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

    Например, если поток пишет в поле 'а', а затем в поле 'b' и значение 'b' не зависит от значения 'a', то компилятор волен изменить порядок этих операций, и кэш имеет право сбросить (flush) 'b' в оперативную память раньше чем 'a'. Есть несколько потенциальных источников переупорядочения, таких как компилятор, JIT и кэш-память.

    Компилятор, среда исполнения и аппаратное обеспечение допускают изменение порядка инструкций при сохранении иллюзии, как-если-последовательной (as-if-serial) семантики, что означает, что однопоточные программы не должны наблюдать эффекты переупорядочения. Тем не менее, изменение порядка следования может вступить в игру в некорректно синхронизированных многопоточных программах, где один поток может наблюдать эффекты производимые другими потоками, и такие программы могут быть в состоянии обнаружить, что переменные становятся видимыми для других потоков в порядке, отличном от указанного в исходном коде.

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

    Что было не так со старой моделью памяти?


    Было несколько серьезных проблем со старой моделью памяти. Она была трудна для понимания и поэтому часто нарушалась. Например, старая модель во многих случаях не позволяла многие виды переупорядочения, которые были реализованы в каждой JVM. Эта путаница со смыслом старой модели привела к тому, что были вынуждены создать JSR-133.

    Было широко распространено мнение, что при использовании final-поля не было необходимости в синхронизации между потоками, чтобы гарантировать, что другой поток будет видеть значение поля. Хотя это разумное предположение о разумном поведении, да и вообще мы бы хотели чтобы все именно так и работало, но под старой моделью памяти, это было просто не правдой. Ничто в старой модели памяти не отличало final-поля от любых других данных в памяти — это значит, что синхронизация была единственный способом обеспечить, чтобы все потоки увидели значение final-поля записанного в конструкторе. В результате, была возможно того, что вы увидите значение поля по умолчанию, а затем через некоторое время увидите присвоенное значение. Это означает, например, что неизменяемые объекты, такие как строки могут менять свое значение — тревожная перспектива.
    Комментарий переводчика
    Ниже будет детально объяснен пример со строками

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

    Наконец, предположения программистов о том, что может произойти, если их программы некорректно синхронизированы часто ошибочны. Одна из целей JSR-133 — обратить внимание на этот факт.

    Что вы подразумеваете под «некорректно синхронизированы»?


    Под некорректно синхронизированным кодом разные люди подразумевают разные вещи. Когда мы говорим о некорректно синхронизированном коде в контексте модели памяти Java, мы имеем в виду любой код, в котором:
    1. есть запись переменной одним потоком,
    2. есть чтение той же самой переменной другим потоком и
    3. чтение и запись не упорядочены по синхронизации (are not ordered by synchronization)

    Когда это происходит, мы говорим, что происходит гонка потоков (data race) на этой переменной. Программы с гонками потоков — некорректно синхронизированные программы.
    Комментарий переводчика
    Надо понимать, что некорректно синхронизированные программы не являются Абсолютным Злом. Их поведение хотя и недерминировано, но все возможные сценарии полностью описаны в JSR-133. Эти поведения зачастую неинтуитивны. Поиск всех возможных результатов алгоритмически весьма сложен и опирается в том числе на такие новые понятия как commitment protocol и causality loops.
    Но в ряде случаев использование некорректно синхронизированных программ, видимо, оправдано. В качестве примера достаточно привести реализацию java.lang.String.hashCode()
    public final class String  implements Serializable, Comparable<String>, CharSequence {
        /** Cache the hash code for the string */
        private int hash; // Default to 0
        ...
        public int hashCode() {
            int h = hash;
            if (h == 0 && count > 0) {
                ...
                hash = h;
            }
            return h;
        }
        ...
    }
    

    При вызове метода hashCode() у одного экземпляра java.lang.String из разных потоков будет гонка потоков (data race) по полю hash.

    Интересна статья Hans-J. Boehm, «Nondeterminism is unavoidable, but data races are pure evil»
    И ее обсуждение на русском
    Руслан Черемин, «Data races is pure evil»


    Что делает синхронизация?


    Синхронизация имеет несколько аспектов. Наиболее хорошо понимаемый является взаимное исключение (mutual exclusion) — только один поток может владеть монитором, таким образом синхронизации на мониторе означает, что как только один поток входит в synchronized-блок, защищенный монитором, никакой другой поток не может войти в блок, защищенный этом монитором пока первый поток не выйдет из synchronized-блока.

    Но синхронизация — это больше чем просто взаимное исключение. Синхронизация гарантирует, что данные записанные в память до или в синхронизированном блоке становятся предсказуемо видимыми для других потоков, которые синхронизируются на том же мониторе. После того как мы выходим из синхронизированного блока, мы освобождаем (release) монитор, что имеет эффект сбрасывания (flush) кэша в оперативную память, так что запись сделанные нашим потоком могут быть видимыми для других потоков. Прежде чем мы сможем войти в синхронизированный блок, мы захватываем (asquire) монитор, что имеет эффект объявления недействительными данных локального процессорного кэша (invalidating the local processor cache), так что переменные будут загружены из основной памяти. Тогда мы сможем увидеть все записи, сделанные видимым предыдущим освобождением (release) монитора.

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

    Семантика новой модели памяти накладывает частичный порядок на операции с памятью (чтение поля, запись поля, захват блокировки (lock), освобождение блокировки (unlock)) и другие операции с потоками (start(), join()). Некоторые действия, как говорят, «происходят прежде» (happens before) других. Когда одно действие «происходит прежде» (happens before) другого, первое будет гарантированно расположено до и видно второму. Правила этого упорядочения таковы:
    Комментарий переводчика
    Частичный порядок — это не оборот речи, а математическое понятие.

    1. Каждое действие в потоке «происходит прежде» (happens before) любого другого действия в этом потоке, которое идет «ниже» в коде этого потока.
    2. Освобождение монитора «происходит прежде» (happens before) каждого последующего захвата того же самого монитора.
    3. Запись в volatile-поле происходит «происходит прежде» (happens before) каждого последующего чтения того же самого volatile-поля.
    4. Вызов метода start() потока «происходит прежде» (happens before) любых действий в запущенном потоке.
    5. Все действия в потоке «происходят прежде» (happens before) любых действий любого другого потока, который успешно завершил ожидание на join() по первому потоку.

    Это означает, что любые операции с памятью, которые были видны в потоке перед выходом из синхронизированного блока видны любому другому потоку после того, как он заходит в синхронизированный блок, защищенный тем же монитором, так как все операции с памятью «произойдут прежде» освобождения монитора, а освобождение монитора «происходит прежде» захвата.
    Комментарий переводчика
    Эта программа (data — volatile, run — volatile) гарантированно остановится и напечатает 1 И в старой И в новой моделях памяти
    public class App {
        static volatile int data = 0;
        static volatile boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = false;
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    


    Эта программа (data — НЕ volatile, run — volatile) гарантированно остановится И в старой И в новой моделях памяти, но в старой может напечатать и 0 и 1, а в новой гарантированно напечатает 1. Это связано с тем, что в новой модели памяти можно «поднимать» запись в не-volatile, «выше» записи в volatile, но нельзя «спускать ниже». А в старой можно было и «поднимать» и «спускать ниже».
    public class App {
        static int data = 0;
        static volatile boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = false;
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    


    Эта программа (data — volatile, run — НЕ volatile) может как остановиться так и не остановиться в обеих моделях. В случае остановки может напечатать как 0 так и 1 и в старой и в новой моделях памяти. Это вызвано тем, что в обеих моделях можно «поднять» запись в не-volatile выше записи в volatile.
    public class App {
        static volatile int data = 0;
        static boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = false;
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    


    Эта программа (data — НЕ volatile, run — НЕ volatile) может как остановиться так и не остановиться в обеих моделях. В случае остановки может напечатать как 0 так и 1 и в старой и в новой моделях памяти.
    public class App {
        static int data = 0;
        static boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = false;
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    


    Другим следствием является то, что следующий шаблон, который некоторые люди используют, чтобы установить барьер памяти, не работает:
    synchronized (new Object()) {}
    

    Это конструкция является на самом деле «пустышкой» (no-op), и ваш компилятор может удалить ее полностью, потому что компилятор знает, что никакой другой поток не будет синхронизироваться на том же мониторе. Вы должны установить отношение «происходит прежде» отношения для одного потока, чтобы увидеть результаты другого.

    Важное примечание: Обратите внимание, важно для обоих потоков синхронизироваться на одном и том же мониторе, чтобы установить отношение «происходит прежде» (happens before relationship) должным образом. Это не тот случай, когда все видимое потоку A, когда он синхронизируется на объекте X становится видно потоку B после того, как тот синхронизирует на объекте Y. Освобождение и захват должны «соответствовать» (то есть, быть выполнены с одним и тем же монитором), чтобы была обеспечена правильная семантика. В противном случае код содержит гонку данных (data race).
    Комментарий переводчика
    Следующая программа может как остановиться, так и не остановиться в рамках обеих моделей памяти (так как в разных потоках происходит захват и освобождение мониторов различных объектов — lockA / lockB)
    public class App {
        static Object lockA = new Object();
        static Object lockB = new Object();
        static boolean run = true;
    
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    synchronized (lockA) {
                        run = false;
                    }
                }
            }).start();
    
            while (true) {
                synchronized (lockB) {
                    if (!run) break;
                }
            }
        }
    }
    



    Как может случиться, что финальная поля меняют значения?


    Один из лучших примеров того, как значения final-полей могут измениться, включает одну конкретную реализацию класса String.

    Строка может быть реализована как объект с тремя полями — массив символов, смещение в этом массиве, и длины. Причиной выбора реализации String таким образом вместо реализации в виде одного поля типа char[], может быть то, что она позволяет нескольким строкам и объектам StringBuffer разделять один и тот же массив символов и избежать дополнительного выделение объекта и копирование памяти. Так, например, метод String.substring() может быть реализован путем создания новой строки, которая разделяет тот же массив символов с исходной строкой и просто отличается полями длина и смещение. У String все три поля — final.
    Комментарий переводчика
    До update 6 для JRE 7 от Oracle java.lang.String реализована вот так
    public final class String implements Serializable, Comparable<String>, CharSequence {
        private final char[] value;
        private final int offset;
        private final int count;
        ....
    }
    

    В начиная с update 6 для JRE 7 от Oracle java.lang.String реализована уже по другому (без полей offset и count)
    О причинах Вы можете прочитать тут. Это не меняет сути примера авторов, так как «содержимое» финального массива value тоже может «пока не долететь» (исключительно в старой модели). Даже возможны несколько стадий:
    1. ссылка на строку уже не null, а поле value — пока null
    2. ссылка на строку уже не null, поле value уже не null, но в некоторых ячейках char[] value не корректные char-ы, а первоначальные 0-и.

    Комментарий переводчика
    Ниже — реализация метода String.substring(), как видим новая строка разделяет со старой массив char[] value
    public final class String implements Serializable, Comparable<String>, CharSequence {
        private final char[] value;
        private final int offset;
        private final int count;
        ....
        public String substring(int beginIndex, int endIndex) {
            ....
            return new String(offset + beginIndex, endIndex - beginIndex, value);
        }
        ....
    }
    


    Ниже — реализация метода StringBuffer.toString(), как видим новый String разделяет со старым StringBuffer массив символов char[] value
    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        char[] value;
        ...
    }
    
    public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {
        private final char[] value;
        private final int offset;
        private final int count;
        ....
        public synchronized String toString() {
            return new String(value, 0, count);
        }
        ....
    }
    


    String s1 = "/usr/tmp";
    String s2 = s1.substring(4); 
    


    Строка s2 будет иметь смещение (offset) равное 4 и длину (length) равную 4. Но в рамках старой модели памяти другому потоку было возможно увидеть значение смещение (offset) по умолчанию (0), а позже увидеть корректное значение 4. Будет казаться, будто строка "/usr" изменилась на "/tmp".

    Первоначальная модель памяти позволяла такое поведение и некоторые JVM его демонстрировали. Новая модель памяти запрещает его.

    Как final-поля работают при новой модели памяти?


    Значения для final-полей объекта задаются в конструкторе. Если предположить, что объект построен «правильно», то как только объект построен, значения, присвоенные final-полям в конструкторе будут видны всем другим потокам без синхронизации. Кроме того, видимые значения для любого другого объекта или массива, на который ссылается эти final-поля будут по крайней мере, так же «свежи», как и значения final-полей.
    Комментарий переводчика
    Значения для final-полей объекта задаются в конструкторе или инициализаторе.
    Так
    public class App {
        final int k;
        public App10(int k) {
            this.k = k;
        }
    }
    

    Или так
    public class App {
        final int k;
        {
            this.k = 42;
        }
    }
    


    Комментарий переводчика
    Эта программа может как остановиться, так и не остановиться (будь instance — volatile, она бы гарантированно остановилась). Но если остановится, то гарантированно напечатает "[1, 2]"
    import java.util.Arrays;
    
    public class App {
        final int[] data;
        public App() {
            this.data = new int[]{1, 2};
        }
    
        static App instance;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    instance = new App();
                }
            }).start();
    
            while (instance == null) {/*NOP*/}
            System.out.println(Arrays.toString(instance.data));
        }
    }
    


    Эта программа так же может как остановиться, так и не остановиться. Но если остановится, то может напечатать как "[1, 0]" так и "[1, 2]". Это связанно с тем, что запись элемента с индексом 1 происходит позже записи в final-поле.
    import java.util.Arrays;
    
    public class App {
        final int[] data;
        public App() {
            this.data = new int[]{1, 0};
            this.data[1] = 2;
        }
    
        static App instance;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    instance = new App();
                }
            }).start();
    
            while (instance == null) {/*NOP*/}
            System.out.println(Arrays.toString(instance.data));
        }
    }
    


    Эта программа как и первая так же может как остановиться, так и не остановиться. Но если остановится, то гарантированно напечатает "[1, 2]". Так как запись элемента с индексом 1 происходит до записи в final-поле.
    import java.util.Arrays;
    
    public class App {
        final int[] data;
        public App() {
            int[] tmp = new int[]{1, 0};
            tmp[1] = 2;
            this.data = tmp;        
        }
    
        static App instance;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    instance = new App();
                }
            }).start();
    
            while (instance == null) {/*NOP*/}
            System.out.println(Arrays.toString(instance.data));
        }
    }
    



    Что значит для объект быть «правильно построенным»? Это просто означает, что ссылка на объект «не утечет» до окончания процесса построения экземпляра (см. Safe Construction Techniques для примеров).
    Комментарий переводчика
    Имеется перевод статьи на русский язык (правда автоматическим переводчиком): «Методы безопасного конструирования».

    Другими словами, не помещайте ссылку на только строящийся объект в любом месте, в котором другой поток может увидеть ее. Не присваивайте ее статическому полю, не регистрируйте объект в качестве слушателя в любом другом объекте, и так далее. Эти задачи должны быть сделаны по завершению конструктора (вне конструктора, после его вызова), не в нем.
    class FinalFieldExample {
        final int x;
        int y;
        static FinalFieldExample f;
        public FinalFieldExample() {
            x = 3;
            y = 4;
        }
        static void writer() {
            f = new FinalFieldExample();
        }
        static void reader() {
            if (f != null) {
                int i = f.x;
                int j = f.y;
            }
        }
    }
    

    Комментарий переводчика
    Предполагается, что вызовы методов reader() и writer() будут происходить «почти одновременно» из разных потоков.


    Этот класс является примером того, как должны использоваться final-поля. Поток, вызывающий метод reader() гарантированно прочитает 3 в f.x, поскольку это final-поле. Но нет гарантий, что прочитает 4 в y, поскольку это не-final-поле. Если бы конструктор класса FinalFieldExample выглядел таким образом:
    public FinalFieldExample() { // bad!
        x = 3;
        y = 4;
        // bad construction - allowing this to escape
        global.obj = this;
    }
    

    тогда нет гарантий, что поток, прочитавший ссылку на данный объект из global.obj прочитает 3 из x.

    Возможность увидеть правильно построенное значение для поля это хорошо, но если данное поле само является ссылкой, то вы также хотите, чтобы ваш код видел «свежее» значение в ссылаемом объекте (или массиве). Вы получаете такую гарантию, если ваше поле — final. Таким образом, вы можете иметь final-ссылку на массив и не беспокоиться, что другие потоки увидят правильное значение для ссылки на массив, но неправильные значения для содержания массива. Опять же, под «правильным» здесь, мы имеем в виду «на момент окончания конструктора объекта», а не «последнего сохраненного значения».

    После всего вышесказанного, хочется сделать замечание, что даже после конструирования неизменного (immutable) объекта (объекта, содержащего исключительно final-поля), если вы хотите убедиться, что все остальные потоки увидят вашу ссылку вам все равно необходимо использовать синхронизацию. Нет другого способа убедиться, что ссылка на неизменный (immutable) объект видна в другом потоке. Гарантии, получаемые вашим кодом от использования final-полей, должны быть глубоко и аккуратно согласованы с понимание того, как вы справляетесь с concurrency в вашем коде.

    Если вы используете JNI для изменения final-поля, то поведение не определено.
    Комментарий переводчика
    Насколько я понимаю, аналогично нет гарантий для случая изменения поля средствами Reflection API.


    Что делает volatile?


    volatile-поля являются специальными полями, которые используются для передачи состояние между потоками. Каждое чтение из volatile возвратит результат последней записи любым другим потоком; по сути, они указываются программистом как поля, для которых не приемлемо увидеть «несвежее» (stale) значение в результате кэширования или переупорядочения. Компилятору и runtime-среде запрещено размещать их в регистрах. Они также должны убедиться, что после записи в volatile данные «проталкиваются» (flushed) из кэша в основную память, поэтому они сразу же становятся видны другим потокам. Аналогично, перед чтением volatile-поля кэш должен быть освобожден, так что мы увидим значение в оперативной памяти, а не в кэше Существуют также дополнительные ограничения на изменение порядка обращения к volatile переменным.

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

    В соответствии с новой моделью памяти, по-прежнему верно, что volatile переменные не могут быть переупорядочены друг с другом. Разница в том, что теперь уже не так легко изменить порядок между обычными полями расположенными рядом volatile. Запись в volatile поле имеет тот же эффект для памяти как и освобождение монитора (monitor release), а чтение из volatile поля имеет тот же эффект для памяти как и захват монитора (monitor acquire). В сущности, так как новая модель накладывает более строгие ограничения на изменение порядка между доступом к volatile полям и другими полями (volatile или обычным), все, что было видимо для потока A когда он писал в volatile поле f становится видимым для потока B, когда он прочтет f.
    Комментарий переводчика
    И в старой и в новой моделях памяти программа гарантированно остановится и напечатает 1 (data — volatile, run — volatile)
    public class App {
        static volatile int data = 0;
        static volatile boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = false;
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    

    И в старой и в новой моделях памяти программа гарантированно остановится. В новой модели гарантированно напечатает 1, в старой может 0 или 1 (data — НЕ volatile, run — volatile), так в новой нельзя переносить запись в не-volatile «ниже» чем запись в volatile, а в старой — можно
    public class App {
        static int data = 0;
        static volatile boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = false;
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    

    И в старой и в новой моделях памяти программа может НЕ остановиться (run — не volatile и может «залипнуть» в кэше). В обеих моделях если остановится, то может напечатать как 1, так и 0 (data — НЕ volatile, run — НЕ volatile), так как можно менять порядок независимых записей в не-volatile поля
    public class App {
        static int data = 0;
        static boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = false;
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    

    Делаем запись во вторую переменную зависимой от записи в первую переменную. И в старой и в новой моделях памяти программа может НЕ остановиться (run — не volatile и может «залипнуть» в кэше). Но теперь в новой модели в случае остановки напечатает гарантированно 1
    public class App {
        static int data = 0;
        static boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = (data != 1);
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    

    И в старой и в новой моделях памяти программа может НЕ остановиться. В обеих моделях если остановится, то может напечатать как 1, так и 0 (data — volatile, run — НЕ volatile), так как можно переносить запись в не-volatile «выше» чем запись в volatile
    public class App {
        static volatile int data = 0;
        static boolean run = true;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                public void run() {
                    data = 1;
                    run = false;
                }
            }).start();
    
            while (run) {/*NOP*/};
            System.out.println(data);
        }
    }
    


    Вот простой пример того, как volatile поля могут быть использованы
    class VolatileExample {
        int x = 0;
        volatile boolean v = false;
        public void writer() {
            x = 42;
            v = true;
        }
        public void reader() {
            if (v == true) {
                //uses x - guaranteed to see 42.
            }
        }
    }
    

    Назовем один поток писателем, а другой — читателем. Запись в v в писателе «сбрасывает» данные x в оперативную память, а чтение v «захватывает» это значение из памяти. Таким образом, если читатель увидит значение true поля v, то также гарантированно увидит значение 42 в x. Это не было верно, для старой моделью памяти (в старой — можно было «спустить» запись в не-volatile «ниже» записи в volatile). Если бы v не было volatile, то компилятор мог бы изменить порядок записи в писателе, и читатель мог бы увидеть 0 в х.
    Комментарий переводчика
    Данный пример также детально разобран в Joshua Bloch «Effective Java» 2nd edition (Item 66: Synchronize access to shared mutable data) или в переводе Джошуа Блох «Java. Эффективное программирование» 1 издание (Совет 48. Синхронизируйте доступ потоков к совместно используемым изменяемым данным)


    Семантика volatile была существенно усиленна, почти до уровня synchronized. Каждое чтение или запись volatile действует как «половина» synchronized с точки зрения видимости.

    Важное примечание: Обратите внимание, важно чтобы оба потока сделали чтение-запись по одной и той же volatile переменной, с целью добиться установления happens before отношения. Это не тот случай, когда все, что видимо для потока А, когда он пишет volatile-поле f становится видимым для потока B после того, как он считает volatile-поле g. Чтение и запись должны относиться к одной и той же volatile-переменной, чтобы иметь должную семантику.

    Решила ли новая модель памяти «double-checked locking» проблему?


    (Печально известная) double-checked locking идиома (также называемая multithreaded singleton pattern) — это трюк, предназначенный для поддержки отложенной инициализации при отсутствии накладных расходов на синхронизацию. В самых ранних JVM синхронизация была медленной и разработчики стремились удалить ее, возможно, слишком ретиво. Double-checked locking идиома выглядит следующим образом:
    // double-checked-locking - don't do this!
    private static Something instance = null;
    public Something getInstance() {
        if (instance == null) {
            synchronized (this) {
                if (instance == null)
                    instance = new Something();
            }
        }
        return instance;
    }
    

    Комментарий переводчика
    Мне кажется вот так корректнее (авторы не предоставили законченный класс, но в «каноническом варианте» метод getInstance() — статический, как следствие в нем невозможна синхронизация по this)
    // double-checked-locking - don't do this!
    public class Something {
        private static Something instance = null;
        public static Something getInstance() {
            if (instance == null) {
                synchronized (Something.class) {
                    if (instance == null)
                        instance = new Something();
                }
            }
            return instance;
        }
        ....
    }
    

    или с использованием идиомы Private Mutex
    // double-checked-locking - don't do this!
    public class Something {
        private static final Object lock = new Object();
        private static Something instance = null;
        public static Something getInstance() {
            if (instance == null) {
                synchronized (lock) {
                    if (instance == null)
                        instance = new Something();
                }
            }
            return instance;
        }
        ....
    }
    



    Это выглядит ужасно умно — мы избегаем синхронизации на наиболее частом пути выполнения. Есть только одна проблема с этим — идиома не работает. Почему? Наиболее очевидной причиной является то, что запись данных, инициализирующих экземпляр и запись ссылки на экземпляра в статическое поле могут быть переупорядочены компилятором или кэшом, что будет иметь эффект возвращения чего-то «частично построенного». Результатом будет то, что мы читаем неинициализированный объект. Есть много других причин, почему некорректна как эта идиома, так и алгоритмические поправки к ней. Нет никакого способа, чтобы исправить это в старой модели памяти Java. Более подробную информацию можно найти «Double-checked locking: Clever, but broken» и тут «The 'Double Checked Locking is broken' declaration».

    Многие полагали, что использование ключевого слова volatile позволит устранить проблемы, которые возникают при попытке использовать шаблон double-checked-locking. В виртуальных машинах до 1.5, volatile все равно не будет гарантировать корректную работу. В соответствии с новой моделью памяти, объявление поля как volatile «исправит» проблемы с double-checked-locking, так как будет установлено отношение «произошло прежде» (happens before) между инициализацией Something конструирующим потоком и возвратом читающему потоку.

    Тем не менее, для любителей double-checked locking (и мы действительно надеемся, что их не осталось), новости по-прежнему не очень хороши. Весь смысл double-checked locking был избежать накладных расходов синхронизации. Мало того, что кратковременная синхронизации теперь НАМНОГО дешевле, чем в Java 1.0, так еще и в рамках новой модели памяти, падение производительности при использовании volatile почти достигло уровня стоимости синхронизации. Так что до сих пор нет хорошей причины для использования с double-checked locking. Отредактировано: volatile обходится дешево на большинстве платформ.

    Взамен используйте Initialization On Demand Holder идиому, которая безопасна и намного проще для понимания:
    private static class LazySomethingHolder {
        public static Something something = new Something();
    }
    
    public static Something getInstance() {
        return LazySomethingHolder.something;
    }
    

    Этот код гарантированно корректен, что следует из гарантий инициализации для статических полей; если поле устанавливается в статическом инициализаторе, он гарантированно сделать его корректно видимым, для любого потока, который обращается к этому классу.
    Комментарий переводчика
    Приведу чуть более полный вариант
    public class Something {
        private static Something instance = null;
        public static Something getInstance() {
            return LazySomethingHolder.something;
        }
        ....
        private static class LazySomethingHolder {
            public static Something something = new Something();
        }
    }
    

    Спецификация языка гарантирует как
    • однократность инициализации статического поля LazySomethingHolder.something
    • так и ее «ленивость»

    гарантии ленивости можно найти в спецификации («12.4.1. When Initialization Occurs»):
    A class or interface type T will be initialized immediately before the first occurrence of any one of the following:
    • ...
    • A static field declared by T is assigned.
    • ...

    A class or interface will not be initialized under any other circumstance.

    Гарантии корректной инициализации в случае многопоточного доступа можно найти в части «12.4.2. Detailed Initialization Procedure» спецификации.


    Что если я пишу виртуальную машину?


    Вам стоит посмотреть на http://gee.cs.oswego.edu/dl/jmm/cookbook.html.

    Почему я должен беспокоиться?


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


    Контакты



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

    skype: GolovachCourses
    email: GolovachCourses@gmail.com

    GolovachCourses

    35,00

    Компания

    Поделиться публикацией

    Похожие публикации

    Комментарии 32
      +3
      Есть по JMM хорошие презентации от TheShade и Walrus здесь: shipilev.net/#jmm
        0
        Подскажите, пожалуйста, по поводу примеров с volatile-не-volatile.

        Правильно ли я понимаю, что в третьем и четвертом вариантах программа может остановиться или не остановиться из-за того, что поток может запускаться после запуска цикла в случае, если run — НЕ volatile?
          0
          Дело не в До или ПОСЛЕ.
          В целях возможности вручную управлять механизмами синхронизации (довольно дорогостоящими) ПО УМОЛЧАНИЮ при отсутствии каких-либо синхронизационных действий — передача данных между кэшами процессоров НЕ ПРЕДПОЛАГАЕТСЯ.
          Т.е. у каждого процессора в локальном кэше содержится своя собственная копия флага. И то, что он в одном потоке уже false, не противоречит тому, что в другом в это же время true.
            0
            Спасибо за разъяснения, теперь понятно.
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Да, я был некорректен сказав «кэши».
                Имел в виду, что в результате пеупорядочения и/или оптимизации кода компилятором/JIT/CPU может возникнуть эффект, будто у вас осталась «ваша копия», а у другого потока «другая копия». Некорректно указывать ГДЕ она «лежит». Спецификации Lang/JMM этого не говорят.
            0
            Интересная особенность на счет volatile, в JAVA это является легальным механизмом синхронизации. В С++ же, оно вроде как и делает что то похожее, но не то что бы не одобряется для данного использования, а просто запрещено для него.
              0
              Кажется, в C++ семантика другая.
              Это указание компилятору на то, что переменная подвержена частому изменению и рекомендуется к размещению в регистрах процессора.
              Т.е. ваш флаг вообще не будет разделяться ядрами. У каждого — своя копия.
                0
                В С++ volatile запрещает компилятору оптимизировать доступ к памяти. Нужно оно если память может быть изменена другим потоком, процессом или если память, например, замаплена на регистр какого-то устройства. Т.е. сементика почти такая же. Есть ещё ключевое слово register, которое рекомендует компилятору разместить переменную в регистре.
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    С этим согласен. Но без volatile из-за компиляторных оптимизаций ты можешь не прочитать новые данные записанные в память. В этом сходство. Хочешь читать память обновляемую, например, датчиком контроллера — будь добр объяви её volatile (ну или выключить оптимизацию прагмой), иначе можешь не получить того, что ожидаешь.

                    Каноничный пример, когда stop может быть изменена в другом потоке/процессе
                    int * stop = (int *) 0xBABABABA; *stop=0; while (!*stop) { /* do smth */ }

                    может быть оптимизирован компилятором в
                    while (1) { /* do smth */ }

                    Практически всегда, когда какая-то область памяти может изменяться в другом потоке/процессе её следует объявлять с volatile. Не буду углубляться, сам могу уже не помнить всех тонкостей, давно не писал на C/C++. Есть немало хороших статей на тему.
                    0
                    Значит был не прав.
                    Меня оправдывает предусмотрительно вставленное
                    Кажется

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

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

                  Нет.

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

                  При первом обращении к классу.

                  и как удовлетворяется условие что это будет сделано перед первым чтение и только единожды?

                  Automagically
                    0
                    Automagically

                    вопрос лишь в том где находится синхронизация, у пользователя или в JVM.
                      0
                      В JVM/платформе. А именно — в коде ClassLoader-а, идет с сорцах JRE.
                      Если интересно — найду.
                        0
                        Благодарствую, но не стоит. Мне бы хотелось услышать (если вы знаете про это) как эта синхронизация работает. В С++ до С++11 была проблема иницализации локальных статических данных, а именно при одновременном запросе к ним из разных тредов, начальная инициализация могла выполниться более одного раза. В С++11 ввели гарантии того, что статическая переменная инициализируется ровно один раз (собстно так же как и в JAVA), Но! Пряники бесплатными не бывают. Раньше такого поведения можно было достич если обращение к статической переменой спрятать под синхронизацией. Но это же медленно! Double check! Но это же не работает! А больше то техник и небыло. Собственно стоит вопрос, а как же тогда это сделали в недрах, например в JVM?

                        Если там синхронизация при каждом доступе к статической переменной? Если нет, то как они умудряются добится единоразовой инициализации при конкурентном доступе?
                          0
                          В Java специфицирован процесс загрузки класса. И в нем четко определена последовательность действий, включая инициализацию статических полей и запуск статических инициализаторов.
                          Каждый класс, загруженный в JVM определяется ПАРОЙ — {«имя-класса»:String, «загрузчик»:java.lang.ClassLoader}.
                          ClassLoader можно получить у любого класса средствами самого языка/платформы.
                          Либо:
                          ClassLoader loader = MyClass.class;
                          Либо:
                          MyClass instance =…
                          ClassLoader loader = instance.getClass(); // Метод определен в предке всех ссылочных типов — java.lang.Object
                          .
                          Когда классу classA требуется classB, то загрузчик класса класса classA автоматически грузит класс classB. При этом происходит «линковка» кода, строковые имена используемых классов превращаются в внутренние структуры JVM представляющие в том числе код методов.
                          .
                          При следующем использовании classB из classA — нет необходимости в вызове процедур ClassLoader (содержащих код синхронизации и, как следствие, «дорогих»), так как во внутреннем представлении класса classA используются не «строковые ссылки» (подлежащие разрешению загрузчиком класса, а ссылки на внутренние структуры JMV (представления класса classB)).
                            0
                            Хм… инициализация статической локальной переменной объявленной внутри метода класса происходит при загрузке класса или же всетаки при первом обращении к методу класса где она объявлена?
                              0
                              статической локальной переменной

                              В Java нет такого. Статические бывают — только поля (не локальные переменные).
                              Тут отличие от С++.
                                0
                                private static class LazySomethingHolder 
                                {
                                        public static Something something = new Something();
                                }
                                

                                т.е. в этом случае something это поле класса? И оно будет инициализаровано когда? При загрузке класса или при обращении к методу?
                                Вы уж простите, просто я особеностей Java незнаю, а тут как бы такая тема которая отчасти пересекается и с С++.
                                  0
                                  Да, это — поле класса.
                                  Момент инициализации определяется по спецификации языка. Там не все просто, надо вчитываться. Ссылку на соответствующий раздел спеки я давал в комментариях.
                                  В конкретном случае — в момент вызова метода, я приводил в комментариях пример кода и его вывода. Посмотрите.
                                  class Test {
                                      public static void main(String[] args) {
                                          System.err.println("Hello!");
                                          System.err.println(Something.getInstance());
                                      }
                                  }
                                  
                                  >> Hello!
                                  >> Exception in thread "main" java.lang.Error
                                  >> ...
                                  

                                  Т.е. исключение (и инициализация экземпляра) происходит при вызове метода getInstance, не ранее (мы успеваем вывести «Hello!»).
                            0
                            Осторожно, разумная гипотеза!: и, видимо, между загруженным классом (тем же самым загрузчиком, но в другом потоке) и первым использованием загруженного класса устанавливается happens-before отношение. Один раз. При первом использовании.
                              0
                              Во первых double check работает — вы просто не умеете его готовить.
                              А во вторых, да некий аналог на синхронизацию первого доступа есть. Но после того как класс инициализирован — он нам не нужен и его больше нет. Так что ничего медленного, никаких оверхедов. Java это managed runtime — что хотим, то и делаем (в пределах спеки).
                                0
                                Во первых double check работает — вы просто не умеете его готовить.

                                Это вы на счет Java или C++. Если второе то покажите рецепт.
                                  0
                                  Мы тут JSR133 обсуждаем.
                                  Но и в C++0x11 все работает (если правильно написать).
                        0
                        Означаетли это что статические поля инициализируются до старта main функции?

                        В Java такой занятный специфицированный мностадийный процесс ленивой загрузки классов. Я давал ссылку по которой можно высчитать когда произойдет.

                        A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

                        Вот из-за выделенной строчки загрузка и произойдет «по требованию». Как следует из остальной части спецификации — в момент первого обращения к полю. В ходе выполнения main().

                        Демонстрация:
                        class Something {
                            private Something() {
                                throw new Error();
                            }
                        
                            public static Something getInstance() {
                                return LazySomethingHolder.something;
                            }
                            private static class LazySomethingHolder {
                                public static Something something = new Something();
                            }
                        }
                        
                        class Test {
                            public static void main(String[] args) {
                                System.err.println("Hello!");
                                System.err.println(Something.getInstance());
                            }
                        }
                        
                        >> Hello!
                        >> Exception in thread "main" java.lang.Error
                        >> ...
                        
                        0
                        Теперь, сказав все это, если после волоске строит неизменный объект (то есть, объект, который содержит только конечные поля), вы хотите убедиться, что он правильно видит все другой поток, вы все равно обычно требуется использовать синхронизацию. Там нет другого пути, чтобы обеспечить, например, о том, что ссылка на неизменяемого объекта будет рассматриваться второго потока. Гарантии программа получает от конечных полей должны быть тщательно закаленное с глубоким и тщательным понимания того, как параллелизм управляется в коде.

                        Простите, что?..
                          0
                          Один не откорректированный абзац.
                          Пропустил.
                          Исправлю.
                          0
                          Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).
                            0
                            Перечитывал топик и нашел ошибку в одном из примеров про описание семантики ключевого слова final, в примере указанном ниже, программа гарантированно напечатает — "[1, 2]" (если остановится), это связанно с тем, что ссылка на объект опубликована безопасно (Safe Pulication) и JLS говорит нам следующее:
                            Объект считается полностью инициализированным, когда завершается его конструктор. Поток, который может видеть ссылку на объект только после его инициализации, гарантированно видит корректно инициализированные значения final-полей этого объекта.
                            То есть, «выход» из конструктора приводит к «замораживанию» (freeze action) нашего массива и другой поток всегда будет «видеть» правильно инициализированное final поле. Вот если добавить в конструктор
                            instance = this;
                            тогда пример «заиграет» новыми красками, так как ссылка на конструируемый объект «утечёт» (leak), в таком случае другой поток сможет «увидеть» любое промежуточное состояние массива: null или [1, 0].
                            
                            public class App {
                                final int[] data;
                                public App() {
                                    this.data = new int[]{1, 0};
                                    this.data[1] = 2;
                                }
                            
                                static App instance;
                                public static void main(String[] args) {
                                    new Thread(new Runnable() {
                                        public void run() {
                                            instance = new App();
                                        }
                                    }).start();
                            
                                    while (instance == null) {/*NOP*/}
                                    System.out.println(Arrays.toString(instance.data));
                                }
                            }
                            
                              0
                              Продолжу, немного в шизофренической манере, также, в выше указанном примере, в последней строчке кода может быть брошен NullPointerException. То есть, если даже в цикле while мы «увидим», что ссылка на instance не null, JMM не гарантирует нам, что уже в следующей строке мы не прочитаем null. Эти два чтения независимы, а ссылка была опубликована в другом потоке, подробнее можно прочитать, про такой случай по ссылке (спасибо Шипилёву, который пишет такие замечательные статьи) или краткая цитата для ленивых:
                              JMM says we can, because the execution producing this outcome does not violate the memory model requirements. Informally, we can say that a decision on what a particular read can observe is made for each read in isolation. Since both reads are racy

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

                            Самое читаемое