Возможна ли энергонезависимая блокировка с двойной проверкой?

Вот мой одноэлементный класс.

Статическое поле instance не является изменчивым, поэтому возникает проблема изменения порядка / видимости. Для ее решения поле экземпляра val делается окончательным. Поскольку экземпляр построен правильно, его клиенты всегда должны видеть инициализированное поле val, если они вообще видят экземпляр.

    static class Singleton {
    private static Singleton instance;
    private final String val;
    public Singleton() { this.val = "foo"; }

    public static Singleton getInstance() {
        if (instance == null)
            synchronized (Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
            }
        }
        return instance;
    }
    public String toString() { return "Singleton: " + val; }
}

Однако есть еще одна проблема - у меня есть два незащищенных чтения поля "instance", которые можно (?) Переупорядочить, чтобы клиент мог получить null вместо реального значения:

public static Singleton getInstance() {
    Singleton temp = instance;
    if (instance != null) return temp;
    else { /* init singleton and return instance*/ }
}

Чтобы обойти это, я чувствую, что могу ввести локальную переменную:

public static Singleton getInstance() {
    Singleton temp = instance;
    if (temp == null)
        synchronized (Singleton.class) {
            if(instance == null) {
                instance = new Singleton();
                temp = instance;
        }
    }
    return temp;
}

Кажется, это решает проблему, поскольку существует только одно незащищенное чтение значения, поэтому ничего действительно плохого не должно происходить. Но ... Я только что изменил поток программы без (почти?) Изменения его однопоточной семантики. Означает ли это, что компилятор может просто отменить мой обходной путь, поскольку это преобразование безопасно, и нет способа заставить этот код работать, не установив правильную связь «происходит раньше» с volatile?


person podcherkLIfe    schedule 22.08.2014    source источник
comment
Начиная с Java 1.5 проблема реализации синглетонов решается с помощью перечислений: stackoverflow.com/a/8027815/32090   -  person Boris Pavlović    schedule 22.08.2014
comment
Вопрос не совсем в синглтонах. Скорее, речь идет о самой двойной проверке блокировки.   -  person podcherkLIfe    schedule 22.08.2014
comment
Эти проблемы невероятно сложно решить. Вот почему мы должны использовать абстрактные конструкции, которые предлагает Java из пакета java.util.concurrent.   -  person Boris Pavlović    schedule 22.08.2014


Ответы (3)


Я не уверен, действительно ли может произойти переупорядочение чтения одной и той же переменной, но гарантируется, что на локальные переменные не повлияют действия других потоков. Даже если такого переупорядочения чтения не происходит, эта гарантия актуальна для каждой переменной, которая может обновляться одновременно, пока вы ее читаете: если вы читаете значение и сохраняете его в локальной переменной, вы можете быть уверены, что значение локальной переменной действительно не менять сразу после этого. Конечно, если значение является ссылкой, эта гарантия не распространяется на поля указанного объекта.

Соответствующее предложение можно найти в JLS §17.4.1:

Локальные переменные (§14.4), формальные параметры метода (§8.4.1) и параметры обработчика исключений (§14.20) никогда не используются совместно между потоками и не зависят от модели памяти.

Итак, ответ - нет, компилятору не разрешено отменять ваш обходной путь введения локальной переменной.

person Holger    schedule 02.09.2014
comment
Ах, спасибо, это именно то, что я искал. - person podcherkLIfe; 03.09.2014

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

public class Singleton {
  private static class Holder {
    static final Singleton instance = new Singleton();
  }

  public Singleton getInstance() {
    return Holder.instance;
  }
}

Класс Holder будет инициализирован (и, следовательно, создан экземпляр) только при первом вызове getInstance().

person Ian Roberts    schedule 22.08.2014

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

Вы используете synchronized(Singleton.class). При synchronized Java гарантирует любое чтение / запись до того, как это ключевое слово будет легко отражено в памяти для задействованных переменных. Поскольку ваш Singleton instance также объявлен на уровне класса, любые его изменения легко видны из другого класса и заносятся в основную память.

person Alex Suo    schedule 22.08.2014
comment
Проблема заключается в чтении значения в первой и последней строках метода. Эти показания находятся за пределами синхронизированного блока, поэтому между чтением и записью не устанавливается никаких отношений "не происходит". Вот почему блокировка с двойной проверкой не работает без ключевого слова volatile. Таким образом, я почти уверен, что проблема существует. - person podcherkLIfe; 22.08.2014
comment
Ваша первая строка - это просто проверка NULL, и экземпляр инициализировался только один раз; так что, если это было легко видно в этой ветке, отлично, у вас все в порядке; в противном случае вы попали в синхронизацию и все еще не проблема. Однако я не понимаю, какую строку вы называете последней. - person Alex Suo; 22.08.2014
comment
Насколько я понимаю правила переупорядочивания, чтение энергонезависимой памяти можно переупорядочивать. Вот почему возможно, что первое чтение вернет ненулевое значение (проверка не удалась), а второе - null. Чтобы проиллюстрировать это, я поместил второй фрагмент кода - он демонстрирует действительное переупорядочение (по крайней мере, я думаю, что он действителен), который нарушает многопоточное приложение (первый поток читает null из поля экземпляра, затем второй поток устанавливает экземпляр в ненулевое значение, затем сначала thread читает ненулевое значение из поля экземпляра и возвращает нулевое значение, хранящееся в temp). Возможно, мои рассуждения неверны, поэтому не стесняйтесь поправлять меня. - person podcherkLIfe; 22.08.2014
comment
Помните два момента: 1. Переупорядочивание не происходит для операторов, разделенных synchronized (). 2. после synchronized () все задействованные переменные сбрасываются в основную память, и значения гарантируются, чтобы быть самыми последними. Итак, ваш первый нулевой второй нулевой случай никогда не происходит. - person Alex Suo; 25.08.2014