Двойная проверка блокировки без летучих

Я прочитал этот вопрос о том, как сделать двойную проверку блокировки:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
    FieldType result = field;
    if (result == null) { // First check (no locking)
        synchronized(this) {
            result = field;
            if (result == null) // Second check (with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}

Моя цель - заставить ленивую загрузку поля (НЕ одиночной) работать без атрибута volatile. Объект поля никогда не изменяется после инициализации.

После некоторого тестирования мой последний подход:

    private FieldType field;

    FieldType getField() {
        if (field == null) {
            synchronized(this) {
                if (field == null)
                    field = Publisher.publish(computeFieldValue());
            }
        }
        return fieldHolder.field;
    }



public class Publisher {

    public static <T> T publish(T val){
        return new Publish<T>(val).get();
    }

    private static class Publish<T>{
        private final T val;

        public Publish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }
}

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


Я проверил это с помощью jcstress. SafeDCLFinal работал как ожидалось, тогда как UnsafeDCLFinal был непоследователен (как и ожидалось). На данный момент я на 99% уверен, что это работает, но, пожалуйста, докажите, что я не прав. Составлено с mvn clean install -pl tests-custom -am и беги с java -XX:-UseCompressedOops -jar tests-custom/target/jcstress.jar -t DCLFinal, Тестовый код ниже (в основном модифицированные классы одиночного тестирования):

/*
 * SafeDCLFinal.java:
 */

package org.openjdk.jcstress.tests.singletons;

public class SafeDCLFinal {

    @JCStressTest
    @JCStressMeta(GradingSafe.class)
    public static class Unsafe {
        @Actor
        public final void actor1(SafeDCLFinalFactory s) {
            s.getInstance(SingletonUnsafe::new);
        }

        @Actor
        public final void actor2(SafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonUnsafe::new));
        }
    }

    @JCStressTest
    @JCStressMeta(GradingSafe.class)
    public static class Safe {
        @Actor
        public final void actor1(SafeDCLFinalFactory s) {
            s.getInstance(SingletonSafe::new);
        }

        @Actor
        public final void actor2(SafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonSafe::new));
        }
    }


    @State
    public static class SafeDCLFinalFactory {
        private Singleton instance; // specifically non-volatile

        public Singleton getInstance(Supplier<Singleton> s) {
            if (instance == null) {
                synchronized (this) {
                    if (instance == null) {
//                      instance = s.get();
                        instance = Publisher.publish(s.get(), true);
                    }
                }
            }
            return instance;
        }
    }
}

/*
 * UnsafeDCLFinal.java:
 */

package org.openjdk.jcstress.tests.singletons;

public class UnsafeDCLFinal {

    @JCStressTest
    @JCStressMeta(GradingUnsafe.class)
    public static class Unsafe {
        @Actor
        public final void actor1(UnsafeDCLFinalFactory s) {
            s.getInstance(SingletonUnsafe::new);
        }

        @Actor
        public final void actor2(UnsafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonUnsafe::new));
        }
    }

    @JCStressTest
    @JCStressMeta(GradingUnsafe.class)
    public static class Safe {
        @Actor
        public final void actor1(UnsafeDCLFinalFactory s) {
            s.getInstance(SingletonSafe::new);
        }

        @Actor
        public final void actor2(UnsafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonSafe::new));
        }
    }

    @State
    public static class UnsafeDCLFinalFactory {
        private Singleton instance; // specifically non-volatile

        public Singleton getInstance(Supplier<Singleton> s) {
            if (instance == null) {
                synchronized (this) {
                    if (instance == null) {
//                      instance = s.get();
                        instance = Publisher.publish(s.get(), false);
                    }
                }
            }
            return instance;
        }
    }
}

/*
 * Publisher.java:
 */

package org.openjdk.jcstress.tests.singletons;

public class Publisher {

    public static <T> T publish(T val, boolean safe){
        if(safe){
            return new SafePublish<T>(val).get();
        }
        return new UnsafePublish<T>(val).get();
    }

    private static class UnsafePublish<T>{
        T val;

        public UnsafePublish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }

    private static class SafePublish<T>{
        final T val;

        public SafePublish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }
}

Протестировано с Java 8, но должно работать по крайней мере с Java 6+. См документы


Но мне интересно, сработает ли это:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldHolder fieldHolder = null;
    private static class FieldHolder{
        public final FieldType field;
        FieldHolder(){
            field = computeFieldValue();
        }
    }

    FieldType getField() {
        if (fieldHolder == null) { // First check (no locking)
            synchronized(this) {
                if (fieldHolder == null) // Second check (with locking)
                    fieldHolder = new FieldHolder();
            }
        }
        return fieldHolder.field;
    }

Или, может быть, даже:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldType field = null;
    private static class FieldHolder{
        public final FieldType field;

        FieldHolder(){
            field = computeFieldValue();
        }
    }

    FieldType getField() {
        if (field == null) { // First check (no locking)
            synchronized(this) {
                if (field == null) // Second check (with locking)
                    field = new FieldHolder().field;
            }
        }
        return field;
    }

Или же:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldType field = null;

    FieldType getField() {
        if (field == null) { // First check (no locking)
            synchronized(this) {
                if (field == null) // Second check (with locking)
                    field = new Object(){
                        public final FieldType field = computeFieldValue();
                    }.field;
            }
        }
        return field;
    }

Я верю, что это будет работать на основе этого документа оракула:

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

4 ответа

Решение

Перво-наперво: то, что вы пытаетесь сделать, в лучшем случае опасно. Я немного нервничаю, когда люди пытаются обмануть с финалами. Язык Java предоставляет вам volatile как инструмент для перехода к согласованности между потоками. Используй это.

В любом случае, соответствующий подход описан в "Безопасной публикации и инициализации в Java" как:

public class FinalWrapperFactory {
  private FinalWrapper wrapper;

  public Singleton get() {
    FinalWrapper w = wrapper;
    if (w == null) { // check 1
      synchronized(this) {
        w = wrapper;
        if (w == null) { // check2
          w = new FinalWrapper(new Singleton());
          wrapper = w;
        }
      }
    }
    return w.instance;
  }

  private static class FinalWrapper {
    public final Singleton instance;
    public FinalWrapper(Singleton instance) {
      this.instance = instance;
    }
  }
}

Это условия непрофессионала, это работает так. synchronized дает правильную синхронизацию, когда мы наблюдаем wrapper как ноль - другими словами, код был бы очевидно правильным, если бы мы вообще отбросили первую проверку и расширили synchronized на весь метод тела. final в FinalWrapper гарантирует, что если мы увидели ненулевой wrapper, он полностью построен, и все Singleton поля видны - это восстанавливается после редкого чтения wrapper,

Обратите внимание, что он переносит FinalWrapper в поле, а не само значение. Если instance должны были быть опубликованы без FinalWrapper все ставки будут отменены (с точки зрения непрофессионала, это преждевременная публикация). Вот почему ваш Publisher.publish не работает: просто помещать значение в окончательное поле, читать его и публиковать небезопасно - это небезопасно - это очень похоже на простую установку instance Написать.

Кроме того, вы должны быть осторожны, чтобы сделать "резервное" чтение под замком, когда вы обнаружите ноль wrapper и используйте его значение. Выполняя второе (третье) чтение wrapper в ответ заявление также разрушило бы правильность, настроив вас на законную гонку.

EDIT: все это, кстати, говорит, что если объект, который вы публикуете, покрыт final -с внутренне, вы можете сократить посредник FinalWrapper и опубликовать instance сам.

РЕДАКТИРОВАТЬ 2: См. Также, LCK10-J. Используйте правильную форму дважды проверенной идиомы блокировки, и некоторые обсуждения в комментариях там.

Короче

Версия кода без volatile или класс оболочки зависит от модели памяти базовой операционной системы, на которой работает JVM.

Версия с классом-оберткой является известной альтернативой, известной как шаблон проектирования Initialization on Demand Holder, и опирается на ClassLoader контракт, что любой данный класс загружается не более одного раза, при первом доступе, и потокобезопасным способом.

Нужда в volatile

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

Чтобы выполнить простой (хотя, возможно, многословный) пример, рассмотрим сценарий с двумя потоками и одним уровнем аппаратного кэширования, где каждый поток имеет свою собственную копию field в этом кеше. Так что уже есть три версии field: один в основной памяти, один в первом экземпляре и один во втором. Я буду ссылаться на них как field М, field А и field Б соответственно.

  1. Начальное состояние
    field М = null
    field A = null
    field B = null
  2. Поток A выполняет первую нулевую проверку, находит field А является нулевым.
  3. Поток А получает блокировку на this,
  4. Поток B выполняет первую нулевую проверку, находит field B ноль.
  5. Поток B пытается получить блокировку на this но обнаруживает, что он удерживается потоком A. Нить B спит.
  6. Поток A выполняет вторую нулевую проверку, находит field А является нулевым.
  7. Тема А назначает field А стоимость fieldType1 и снимает блокировку. поскольку field не является volatile это назначение не распространяется.
    field М = null
    field A = fieldType1
    field B = null
  8. Поток B просыпается и получает блокировку на this,
  9. Поток B выполняет вторую нулевую проверку, находит field B ноль.
  10. Тема B назначает field В стоимость fieldType2 и снимает блокировку.
    field М = null
    field A = fieldType1
    field B = fieldType2
  11. В какой-то момент записи в кэш-копию A синхронизируются обратно в основную память.
    field М = fieldType1
    field A = fieldType1
    field B = fieldType2
  12. В какой-то более поздний момент записи в кэш-копию B синхронизируются с основной памятью, перезаписывая назначение, сделанное копией A.
    field М = fieldType2
    field A = fieldType1
    field B = fieldType2

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

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

Инициализация по требованию владельца

Главное, что я хотел здесь отметить, это то, что это работает, потому что мы, по сути, внедряем синглтон в микс. ClassLoader контракт означает, что в то время как может быть много случаев Class может быть только один экземпляр Class<A> доступно для любого типа A, который также загружается первым при первой ссылке / лениво инициализируется. Фактически, вы можете думать о любом статическом поле в определении класса как о действительно полях в синглтоне, ассоциированном с этим классом, где между этими синглтоном и экземплярами класса возникают повышенные привилегии доступа к элементу.

Цитирование Декларации "Двойная проверка заблокирована", упомянутой @Kicsi, самый последний раздел:

Дважды проверенные блокирующие неизменяемые объекты

Если Helper является неизменным объектом, так что все поля Helper являются окончательными, тогда двойная проверка блокировки будет работать без использования изменяемых полей. Идея состоит в том, что ссылка на неизменяемый объект (такой как String или Integer) должна вести себя почти так же, как int или float; чтение и запись ссылок на неизменяемые объекты являются атомарными.

(акцент мой)

поскольку FieldHolder неизменен, вам действительно не нужно volatile ключевое слово: другие потоки всегда будут видеть правильно инициализированные FieldHolder, Насколько я понимаю, FieldType таким образом, всегда будет инициализироваться, прежде чем он будет доступен из других потоков через FieldHolder,

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

Если он неизменен, то вам не нужно FieldHolder вообще, следуя приведенной выше цитате.

Использование Enum или вложенного помощника статического класса для отложенной инициализации, в противном случае просто используйте статическую инициализацию, если инициализация не потребует больших затрат (пространства или времени).

public enum EnumSingleton {
    /**
     * using enum indeed avoid reflection intruding but also limit the ability of the instance;
     */
    INSTANCE;

    SingletonTypeEnum getType() {
        return SingletonTypeEnum.ENUM;
    }
}

/**
 * Singleton:
 * The JLS guarantees that a class is only loaded when it's used for the first time
 * (making the singleton initialization lazy)
 *
 * Thread-safe:
 * class loading is thread-safe (making the getInstance() method thread-safe as well)
 *
 */
private static class SingletonHelper {
    private static final LazyInitializedSingleton INSTANCE = new LazyInitializedSingleton();
}

Декларация "Двойная проверка блокировки сломана"

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

  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }

Нет, это не сработает.

final не гарантирует видимость между потоками, что делает volatile. В цитируемом вами документе Oracle говорится, что другие потоки всегда будут видеть правильно сконструированную версию конечных полей объекта. final гарантирует, что все конечные поля были созданы и установлены к тому времени, когда конструктор объектов завершил работу. Так что если объект Foo содержит последнее поле bar, bar гарантированно будет построено к тому времени Fooконструктор закончил.

Объект, на который ссылается final хотя поле все еще изменчиво, и запись в этот объект может быть неправильно видна в разных потоках.

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

Другие вопросы по тегам