Подтвердить что ты не робот

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

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

// 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 был непоследовательным (как и ожидалось). На данный момент im 99% уверен, что это работает, но, пожалуйста, докажите, что я неправ. Скомпилирован с mvn clean install -pl tests-custom -am и запускается с java -XX:-UseCompressedOops -jar tests-custom/target/jcstress.jar -t DCLFinal. Ниже приведен код тестирования (в основном модифицированные классы тестирования Singleton):

/*
 * 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;
    }

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

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

4b9b3361

Ответ 1

Прежде всего: то, что вы пытаетесь сделать, в лучшем случае опасно. Я немного нервничаю, когда люди пытаются обмануть финал. Язык 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 как null - другими словами, код будет, очевидно, правильным, если мы полностью отбросим первую проверку и продолжим synchronized ко всему телу метода. final в FinalWrapper гарантирует, что если бы мы увидели ненулевое значение wrapper, оно полностью построено, и все поля Singleton видны - это восстанавливается из достоверного чтения wrapper.

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

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

РЕДАКТ: Все это, кстати, говорит, что если объект, который вы публикуете, покрывается внутри final -s внутри, вы можете разрезать посредника FinalWrapper и опубликовать сам instance.

EDIT 2: См. также LCK10-J. Используйте правильную форму двунаправленной блокировки idiom, а также некоторое обсуждение в комментариях там.

Ответ 2

Короче

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

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

Потребность в volatile

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

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

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

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

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

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

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

Ответ 3

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

Двойная проверка блокировки неизменяемых объектов

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

(акцент мой)

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

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

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

Ответ 4

Использование 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 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;
        }
    }

Ответ 5

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

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

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

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