Pull to refresh

Comments 32

Подскажите, пожалуйста, по поводу примеров с volatile-не-volatile.

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

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

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

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

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

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

Нет.

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

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

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

Automagically
Automagically

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

Если там синхронизация при каждом доступе к статической переменной? Если нет, то как они умудряются добится единоразовой инициализации при конкурентном доступе?
В 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)).
Хм… инициализация статической локальной переменной объявленной внутри метода класса происходит при загрузке класса или же всетаки при первом обращении к методу класса где она объявлена?
статической локальной переменной

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

т.е. в этом случае something это поле класса? И оно будет инициализаровано когда? При загрузке класса или при обращении к методу?
Вы уж простите, просто я особеностей Java незнаю, а тут как бы такая тема которая отчасти пересекается и с С++.
Да, это — поле класса.
Момент инициализации определяется по спецификации языка. Там не все просто, надо вчитываться. Ссылку на соответствующий раздел спеки я давал в комментариях.
В конкретном случае — в момент вызова метода, я приводил в комментариях пример кода и его вывода. Посмотрите.
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!»).
Осторожно, разумная гипотеза!: и, видимо, между загруженным классом (тем же самым загрузчиком, но в другом потоке) и первым использованием загруженного класса устанавливается happens-before отношение. Один раз. При первом использовании.
Во первых double check работает — вы просто не умеете его готовить.
А во вторых, да некий аналог на синхронизацию первого доступа есть. Но после того как класс инициализирован — он нам не нужен и его больше нет. Так что ничего медленного, никаких оверхедов. Java это managed runtime — что хотим, то и делаем (в пределах спеки).
Во первых double check работает — вы просто не умеете его готовить.

Это вы на счет Java или C++. Если второе то покажите рецепт.
Мы тут JSR133 обсуждаем.
Но и в C++0x11 все работает (если правильно написать).
Означаетли это что статические поля инициализируются до старта 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
>> ...
Теперь, сказав все это, если после волоске строит неизменный объект (то есть, объект, который содержит только конечные поля), вы хотите убедиться, что он правильно видит все другой поток, вы все равно обычно требуется использовать синхронизацию. Там нет другого пути, чтобы обеспечить, например, о том, что ссылка на неизменяемого объекта будет рассматриваться второго потока. Гарантии программа получает от конечных полей должны быть тщательно закаленное с глубоким и тщательным понимания того, как параллелизм управляется в коде.

Простите, что?..
Один не откорректированный абзац.
Пропустил.
Исправлю.
Перечитывал топик и нашел ошибку в одном из примеров про описание семантики ключевого слова 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));
    }
}
Продолжу, немного в шизофренической манере, также, в выше указанном примере, в последней строчке кода может быть брошен 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
Sign up to leave a comment.