Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 1.06.2014 | нет комментариев
Добрый день.
В рамках комплекта на курс «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)».
Ну и да, приходите учиться ко мне!

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) блокировки; они заметны для программистов на языках высокого яруса.

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

Что подразумевается под „переупорядочением“ (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-блок, защищенный монитором, никакой иной поток не может войти в блок, защищенный этом монитором пока 1-й поток не выйдет из synchronized-блока.

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

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

Семантика новой модели памяти накладывает частичный порядок на операции с памятью (чтение поля, запись поля, завладение блокировки (lock), освобождение блокировки (unlock)) и другие операции с потоками (start(), join()). Некоторые действия, как говорят, «происходят раньше» (happen before) других. Когда одно действие «происходит раньше» (happen 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).

Комментарий переводчика

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

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) — это трюк, предуготовленный для поддержки отложенной инициализации при о_permark!}
Спецификация языка гарантирует как

  • однократность инициализации статического поля 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.

Отчего я должен волноваться?


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

Контакты


skype: GolovachCourses
email: GolovachCourses@gmail.com

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB