Редкое использование WeakReference?

Nov 07 2020

У меня есть класс, экземпляры которого инициализируются и используются базовой плоской формой.

class MyAttributeConverter implements AttributeConverter<XX, YY> {

    public YY convertToDatabaseColumn(XX attribute) { return null; }

    public XX convertToEntityAttribute(YY dbData) { return null; }
}

Все в порядке, и я подумал, что мне нужно добавить несколько статических методов для использования в качестве ссылок на методы.

    private static MyAttributeConverter instance;

    // just a lazy-initialization;
    // no synchronization is required;
    // multiple instantiation is not a problem;
    private static MyAttributeConverter instance() {
        if (instance == null) {
            instance = new MyAttributeConverter();
        }
        return instance;
    }

    // do as MyAttributeConverter::toDatabaseColumn(xx)

    public static YY toDatabaseColumn(XX attribute) {
        return instance().convertToDatabaseColumn(attribute);
    }

    public static XX toEntityAttribute(YY dbData) {
        return instance().convertToEntityAttribute(attribute);
    }

По-прежнему ничего не кажется неправильным (я считаю), и мне не нравится instanceупорство с классом, и поэтому я пытаюсь это сделать.

    private static WeakReference<MyAttributeConverter> reference;

    public static <R> R applyInstance(Function<? super MyAttributeConverter, ? extends R> function) {
        MyAttributeConverter referent;
        if (reference == null) {
            referent = new MyAttributeConverter();
            refernce = new WeakReference<>(referent);
            return applyInstance(function);
        }
        referent = reference.get();
        if (referent == null) {
            referent = new MyAttributeConverter();
            refernce = new WeakReference<>(referent);
            return applyInstance(function);
        }
        return function.apply(referent); // @@?
    }

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

  • Это подход (правильный / неправильный)?
  • Есть ли шанс, что reference.get()внутри function.applyидиомы может быть null?
  • Есть ли вероятность, что могут быть проблемы, такие как утечка памяти?
  • Должен ли я полагаться, SoftReferenceа не на WeakReference?

Спасибо.

Ответы

5 Holger Nov 24 2020 at 20:32

Обратите внимание, что такой метод, как

// multiple instantiation is not a problem;
private static MyAttributeConverter instance() {
    if (instance == null) {
        instance = new MyAttributeConverter();
    }
    return instance;
}

не является потокобезопасным, так как имеет два чтения instanceполя; каждый из них может воспринимать обновления, сделанные другими потоками, или нет. Это означает , что первое чтение в instance == nullможет воспринимать новое значение , записанное другой поток , тогда как вторые в return instance;могли оценить к предыдущему значению, то есть null. Таким образом, этот метод может возвращаться, nullкогда его одновременно выполняют несколько потоков. Это редкий угловой случай, тем не менее, этот метод небезопасен. Вам понадобится локальная переменная, чтобы гарантировать, что тест и оператор return используют одно и то же значение.

// multiple instantiation is not a problem;
private static MyAttributeConverter instance() {
    MyAttributeConverter current = instance;
    if (current == null) {
        instance = current = new MyAttributeConverter();
    }
    return current;
}

Это по-прежнему безопасно, только когда MyAttributeConverterнеизменяемо с использованием только finalполей. В противном случае поток может вернуть экземпляр, созданный другим потоком, в не полностью сконструированном состоянии.

Вы можете использовать простой способ сделать его безопасным без этих ограничений:

private static final MyAttributeConverter instance = new MyAttributeConverter();

private static MyAttributeConverter instance() {
    return instance;
}

Это все еще лениво, поскольку инициализация класса происходит только при одном из указанных триггеров , то есть при первом вызове метода instance().


Использование вами WeakReferenceподвержено тем же проблемам. Кроме того, не ясно, почему вы прибегаете к рекурсивному вызову вашего метода в двух точках, где у вас уже есть требуемый аргумент в локальной переменной.

Правильная реализация может быть намного проще:

private static WeakReference<MyAttributeConverter> reference;

public static <R> R applyInstance(
    Function<? super MyAttributeConverter, ? extends R> function) {

    WeakReference<MyAttributeConverter> r = reference;
    MyAttributeConverter referent = r != null? r.get(): null;      
    if (referent == null) {
        referent = new MyAttributeConverter();
        reference = new WeakReference<>(referent);
    }
    return function.apply(referent);
}

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

Просто подумай

public static <R> R applyInstance(
    Function<? super MyAttributeConverter, ? extends R> function) {

    return function.apply(new MyAttributeConverter());
}

По крайней мере, стоит попробовать измерить производительность приложения и сравнить ее с другими подходами.

С другой стороны, не похоже, что экземпляр занимал значительный объем памяти или удерживал ресурсы, не относящиеся к памяти. В противном случае вас больше беспокоила возможность облета нескольких экземпляров. Итак, другой вариант, который стоит попробовать и сравнить, - это тот, который показан выше, с использованием static finalполя с ленивой инициализацией класса и без возможности собрать этот небольшой объект в мусор.


Последнее уточнение. Ты спрашивал

Есть ли шанс, что reference.get()внутри function.applyидиомы может быть null?

Поскольку reference.get()внутри оценки нет никакого вызова function.apply, нет никаких шансов, что такой вызов может быть оценен nullв этот момент. Функция получает сильную ссылку, и поскольку вызывающий код гарантирует, что этой сильной ссылки нет null, она никогда не будет сделана nullво время вызова applyметода.

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

Но, поскольку вы конкретно спросили reference.get(), сборщик мусора может собирать объект после его последнего использования , независимо от выполнения методов или локальных областей . Таким образом, референт может быть собран во время выполнения applyметода, когда этот метод больше не использует объект. Оптимизация во время выполнения может позволить этому произойти раньше, чем вы могли догадаться, глядя на исходный код , потому что то, что может выглядеть как использование объекта (например, чтение поля), может не использовать объект во время выполнения (например, потому что это значение уже хранится в Регистр ЦП, устраняющий необходимость доступа к памяти объекта). Как уже говорилось, все без изменения поведения метода.

Таким образом, гипотетический reference.get()во время выполнения applyметода в принципе может оцениваться null, но нет причин для беспокойства, как сказано, поведение applyметода не меняется. JVM будет сохранять память объекта столько времени, сколько необходимо для обеспечения правильного выполнения метода.

Но это объяснение было просто для полноты. Как было сказано, вы не должны использовать слабые или мягкие ссылки для объектов, не содержащих дорогостоящих ресурсов.