Um uso raro de WeakReference?

Nov 07 2020

Eu tenho uma classe cujas instâncias são inicializadas e usadas por flatform subjacente.

class MyAttributeConverter implements AttributeConverter<XX, YY> {

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

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

Nada está errado e achei que preciso adicionar alguns métodos estáticos para serem usados ​​como referências de método.

    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);
    }

Mesmo assim nada parece errado (eu acredito) e não gosto de instancepersistir com a aula e é por isso que estou tentando fazer isso.

    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); // @@?
    }

Eu basicamente nem sei como testar esse código. E sinto muito por minhas perguntas, que podem ser um tanto vagas.

  • Esta é uma abordagem (certa / errada)?
  • Existe alguma chance de que reference.get()dentro do function.applyidioma possa estar null?
  • Existe alguma chance de que possa haver alguns problemas, como vazamento de memória?
  • Devo confiar em, em SoftReferencevez de WeakReference?

Obrigado.

Respostas

5 Holger Nov 24 2020 at 20:32

Observe que um método como

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

não é seguro para thread, pois tem duas leituras do instancecampo; cada um deles pode perceber atualizações feitas por outros tópicos ou não. Isto implica que a primeira leitura no instance == nullpode perceber um valor mais recente escrito por outro segmento enquanto que o segundo em return instance;poderia avaliar ao valor anterior, ou seja null. Portanto, esse método pode retornar nullquando mais de um thread o estiver executando simultaneamente. Este é um caso raro, ainda assim, este método não é seguro. Você precisaria de uma variável local para garantir que o teste e a instrução de retorno usem o mesmo valor.

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

Isso ainda é seguro apenas quando MyAttributeConverteré imutável usando apenas finalcampos. Caso contrário, um thread pode retornar uma instância criada por outro thread em um estado construído de forma incompleta.

Você pode usar a maneira simples de torná-lo seguro sem essas restrições:

private static final MyAttributeConverter instance = new MyAttributeConverter();

private static MyAttributeConverter instance() {
    return instance;
}

Isso ainda é lento, pois a inicialização da classe só acontece em um dos gatilhos especificados , ou seja, a primeira chamada do método instance().


O uso de WeakReferenceestá sujeito aos mesmos problemas. Além disso, não está claro por que você recorre a uma invocação recursiva de seu método em dois pontos onde você já tem o argumento necessário em uma variável local.

Uma implementação correta pode ser muito mais simples:

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);
}

Mas antes de usá-lo, você deve reconsiderar se o código complicado vale o esforço. O fato de você aceitar a necessidade de reconstruir o objeto quando ele foi coletado como lixo, mesmo potencialmente construindo várias instâncias em invocações simultâneas, sugere que você sabe que a construção será barata. Quando a construção é barata, provavelmente você não precisa armazenar em cache uma instância dela.

Apenas considere

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

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

Pelo menos vale a pena tentar, medir o desempenho do aplicativo e compará-lo com as outras abordagens.

Por outro lado, não parece que a instância estava ocupando uma quantidade significativa de memória nem contendo recursos que não eram de memória. Do contrário, você estava mais preocupado com a possibilidade de várias instâncias voando. Portanto, a outra variante que vale a pena tentar e comparar é a mostrada acima usando um static finalcampo com inicialização de classe preguiçosa e sem oportunidade de coletar como lixo aquele pequeno objeto.


Um último esclarecimento. Você perguntou

Existe alguma chance de que reference.get()dentro do function.applyidioma possa estar null?

Como não há reference.get()invocação dentro da avaliação de function.apply, não há chance de que tal invocação possa ser avaliada nullneste ponto. A função recebe uma referência forte e, como o código de chamada garantiu que essa referência não nullfosse, ela nunca se tornará nulldurante a invocação do applymétodo.

Geralmente, o coletor de lixo nunca altera o estado do aplicativo de forma que o código que usa referências fortes perceba a diferença (deixando de lado a disponibilidade de mais memória).

Mas, como você perguntou especificamente sobre reference.get(), um coletor de lixo pode coletar um objeto após seu último uso , independentemente das execuções do método ou escopos locais . Assim, o referente pode ser coletado durante a execução do applymétodo quando este método não usa mais o objeto. As otimizações de tempo de execução podem permitir que isso aconteça mais cedo do que você imagina olhando para o código-fonte , porque o que pode parecer um uso de objeto (por exemplo, um campo lido) pode não usar o objeto em tempo de execução (por exemplo, porque esse valor já é mantido em um Registro da CPU, eliminando a necessidade de acesso à memória do objeto). Como disse, tudo sem alterar o comportamento do método.

Portanto, um hipotético reference.get()durante a execução do applymétodo poderia, em princípio, avaliar a null, mas não há motivo para preocupação, como disse, o comportamento do applymétodo não muda. A JVM reterá a memória do objeto enquanto for necessário para garantir a execução correta do método.

Mas essa explicação era apenas para ser completa. Como dito, você não deve usar referências fracas nem suaves para objetos que não contenham recursos caros.