Ленивая инициализация / запоминание без volatile - PullRequest
0 голосов
/ 09 января 2019

Похоже, что модель памяти Java не определяет «обновление» и «очистку» локального кэша, вместо этого люди просто называют это так для простоты, но на самом деле отношение «происходит до» подразумевает обновление и очистку каким-либо образом (будет было бы здорово, если бы вы могли это объяснить, но не прямо в вопросе).

Это меня очень смущает в сочетании с тем, что раздел о модели памяти Java в JLS написан не так, чтобы его было легко понять.

Поэтому не могли бы вы мне сказать, верны ли предположения, которые я сделал в следующем коде, и, следовательно, гарантированно ли он работает правильно?

Это частично основано на коде, предоставленном в статье Википедии о Двойная проверка блокировки , однако там автор использовал класс-оболочку (FinalWrapper), но причина этого не совсем очевидна мне. Может быть, для поддержки null значений?

public class Memoized<T> {
    private T value;
    private volatile boolean _volatile;
    private final Supplier<T> supplier;

    public Memoized(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T get() {
        /* Apparently have to use local variable here, otherwise return might use older value
         * see https://jeremymanson.blogspot.com/2008/12/benign-data-races-in-java.html
         */
        T tempValue = value;

        if (tempValue == null) {
            // Refresh
            if (_volatile);
            tempValue = value;

            if (tempValue == null) {
                // Entering refreshes, or have to use `if (_volatile)` again?
                synchronized (this) {
                    tempValue = value;

                    if (tempValue == null) {
                        value = tempValue = supplier.get();
                    }

                    /* 
                     * Exit should flush changes
                     * "Flushing" does not actually exists, maybe have to use  
                     * `_volatile = true` instead to establish happens-before?
                     */
                }
            }
        }

        return tempValue;
    }
}

Также я прочитал, что вызов конструктора может быть встроен и переупорядочен, что приводит к ссылке на неинициализированный объект (см. этот комментарий в блоге ). Тогда безопасно ли напрямую присваивать результат поставщику или это нужно сделать в два этапа?

value = tempValue = supplier.get();

Два шага:

tempValue = supplier.get();
// Reorder barrier, maybe not needed?
if (_volatile);
value = tempValue;

Редактировать: Название этого вопроса немного вводит в заблуждение, цель состояла в том, чтобы уменьшить использование изменчивого поля. Если инициализированное значение уже находится в кэше потока, то к value осуществляется прямой доступ без необходимости повторного поиска в основной памяти.

Ответы [ 2 ]

0 голосов
/ 29 мая 2019

Ваш код не является потокобезопасным, что может быть легко показано путем удаления всех несущественных частей:

public class Memoized<T> {
    private T value;
    // irrelevant parts omitted

    public T get() {
        T tempValue = value;

        if (tempValue == null) {
            // irrelevant parts omitted
        }

        return tempValue;
    }
}

Таким образом, value не имеет модификатора volatile, и вы читаете его в методе get() без синхронизации, а если он не является null, продолжайте использовать его без какой-либо синхронизации.

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

Тот факт, что вы используете эзотерические конструкции, такие как if (_volatile);, становится неактуальным, поскольку код уже нарушен.

Причина, по которой в примере в Википедии используется оболочка с полем final, заключается в том, что неизменяемые объекты, использующие только поля final, защищены от гонок данных и, следовательно, единственной конструкции, которая безопасна при чтении своей ссылки без действия синхронизации. .

Обратите внимание, что, поскольку лямбда-выражения попадают в одну и ту же категорию, вы можете использовать их, чтобы упростить пример для вашего варианта использования:

public class Memoized<T> {
    private boolean initialized;
    private Supplier<T> supplier;

    public Memoized(Supplier<T> supplier) {
        this.supplier = () -> {
            synchronized(this) {
                if(!initialized) {
                    T value = supplier.get();
                    this.supplier = () -> value;
                    initialized = true;
                }
            }
            return this.supplier.get();
        };
    }

    public T get() {
        return supplier.get();
    }
}

Здесь supplier.get() в пределах Memoized.get() может считывать обновленное значение supplier без действия синхронизации, и в этом случае оно будет читать правильное value, поскольку оно неявно final. Если метод считывает устаревшее значение для ссылки supplier, он заканчивается в блоке synchronized(this), который использует флаг initialized, чтобы определить, необходима ли оценка исходного поставщика.

Поскольку поле initialized будет доступно только в пределах блока synchronized(this), оно всегда будет иметь правильное значение. Этот блок будет выполняться не более одного раза для каждого потока, тогда как только первый будет оценивать get() для исходного поставщика. После этого каждый поток будет использовать поставщика () -> value, возвращая значение без каких-либо действий по синхронизации.

0 голосов
/ 09 января 2019

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

enum LazyX {
   ;
   static volatile Supplier<X> xSupplier; // set somewhere before use

   static class Holder {
       static final X x = xSupplier.get();
   }

   public static X get() {
       return Holder.x;
   }
}

Если вы знаете Поставщика, это становится проще

enum LazyXpensive {
   ;

   // called only once in a thread safe manner
   static final Xpensive x = new Xpensive();

   // after class initialisation, this is a non volatile read
   public static Xpensive get() {
       return x;
   }
}

Вы можете избежать превращения поля в изменчивое, используя Unsafe

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.function.Supplier;

public class LazyHolder<T> {
    static final Unsafe unsafe = getUnsafe();
    static final long valueOffset = getValueOffset();

    Supplier<T> supplier;
    T value;

    public T get() {
        T value = this.value;
        if (value != null) return value;

        return getOrCreate();
    }

    private T getOrCreate() {
        T value;
        value = (T) unsafe.getObjectVolatile(this, valueOffset);
        if (value != null) return value;

        synchronized (this) {
            value = this.value;
            if (value != null) return value;
            this.value = supplier.get();
            supplier = null;
            return this.value;
        }
    }


    public static Unsafe getUnsafe() {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new AssertionError(e);
        }
    }

    private static long getValueOffset() {
        try {
            return unsafe.objectFieldOffset(LazyHolder.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            throw new AssertionError(e);
        }
    }
}

Однако дополнительный поиск - это микрооптимизация. Если вы хотите выполнить синхронизацию один раз для каждого потока, вы можете вообще избежать использования volatile.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...