¿Un uso poco común de WeakReference?

Nov 07 2020

Tengo una clase cuyas instancias son inicializadas y utilizadas por flatform subyacente.

class MyAttributeConverter implements AttributeConverter<XX, YY> {

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

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

Nada está mal y pensé que necesitaba agregar algunos métodos estáticos para usarlos como referencias de métodos.

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

Todavía nada parece estar mal (creo) y no me gusta la instancepersistencia con la clase y es por eso que estoy tratando de hacer esto.

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

Básicamente, ni siquiera sé cómo probar este código. Y lo siento por mis preguntas, las cuales pueden ser algo vagas.

  • ¿Es este un enfoque (correcto / incorrecto)?
  • ¿Existe alguna posibilidad de que reference.get()dentro del function.applyidioma pueda estar null?
  • ¿Existe la posibilidad de que haya algunos problemas como pérdida de memoria?
  • ¿Debo confiar en SoftReferencemás que WeakReference?

Gracias.

Respuestas

5 Holger Nov 24 2020 at 20:32

Tenga en cuenta que un método como

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

no es seguro para subprocesos, ya que tiene dos lecturas del instancecampo; cada uno de ellos puede percibir las actualizaciones realizadas por otros hilos o no. Esto implica que la primera lectura instance == nullpuede percibir un valor más nuevo escrito por otro hilo, mientras que la segunda return instance;puede evaluar el valor anterior, es decir null. Por lo tanto, este método podría regresar nullcuando más de un hilo lo esté ejecutando al mismo tiempo. Este es un caso de esquina poco común, aún así, este método no es seguro. Necesitaría una variable local para asegurarse de que la prueba y la declaración de retorno usen el mismo valor.

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

Esto solo es seguro cuando MyAttributeConverteres inmutable usando solo finalcampos. De lo contrario, un hilo puede devolver una instancia creada por otro hilo en un estado de construcción incompleta.

Puede utilizar la forma sencilla de hacerlo seguro sin esas restricciones:

private static final MyAttributeConverter instance = new MyAttributeConverter();

private static MyAttributeConverter instance() {
    return instance;
}

Esto todavía es lento ya que la inicialización de la clase solo ocurre en uno de los disparadores especificados , es decir, la primera invocación del método instance().


Su uso de WeakReferenceestá sujeto a los mismos problemas. Además, no está claro por qué recurre a una invocación recursiva de su método en dos puntos donde ya tiene el argumento requerido en una variable local.

Una implementación correcta puede ser mucho más sencilla:

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

Pero antes de usarlo, debe reconsiderar si el código complicado vale la pena el esfuerzo. El hecho de que esté aceptando la necesidad de reconstruir el objeto cuando se ha recolectado basura, incluso construyendo potencialmente múltiples instancias en invocaciones concurrentes, sugiere que sabe que la construcción será barata. Cuando la construcción es barata, probablemente no necesite almacenar en caché una instancia de ella.

Solo considera

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

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

Al menos vale la pena intentarlo, medir el rendimiento de la aplicación y compararlo con los otros enfoques.

Por otro lado, no parece que la instancia esté ocupando una cantidad significativa de memoria ni contenga recursos que no sean de memoria. De lo contrario, estaba más preocupado por la posibilidad de que volaran varias instancias. Entonces, la otra variante que vale la pena probar y comparar es la que se muestra arriba usando un static finalcampo con inicialización de clase perezosa y sin oportunidad de recolectar basura ese pequeño objeto.


Una última aclaración. Tu preguntaste

¿Existe alguna posibilidad de que reference.get()dentro del function.applyidioma pueda estar null?

Dado que no hay una reference.get()invocación dentro de la evaluación de function.apply, no hay posibilidad de que dicha invocación se evalúe nullen este punto. La función recibe una referencia fuerte y dado que el código de llamada aseguró que esta referencia fuerte no lo es null, nunca se convertirá nulldurante la invocación del applymétodo.

Generalmente, el recolector de basura nunca alterará el estado de la aplicación de manera que el código que usa referencias fuertes notará una diferencia (dejando a un lado la disponibilidad de más memoria).

Pero como preguntaste específicamente sobre reference.get(), un recolector de basura puede recolectar un objeto después de su último uso , independientemente de las ejecuciones de métodos o los ámbitos locales . Por lo tanto, el referente podría recopilarse durante la ejecución del applymétodo cuando este método ya no usa el objeto. Las optimizaciones en tiempo de ejecución pueden permitir que esto suceda antes de lo que imagina mirando el código fuente , porque lo que puede parecer un uso de objeto (por ejemplo, una lectura de campo) puede no usar el objeto en tiempo de ejecución (por ejemplo, porque ese valor ya está guardado en un Registro de CPU, eliminando la necesidad de acceder a la memoria del objeto). Como se dijo, todo sin alterar el comportamiento del método.

Entonces, un hipotético reference.get()durante la ejecución del applymétodo podría en principio evaluar a null, pero no hay motivo de preocupación, como se dijo, el comportamiento del applymétodo no cambia. La JVM retendrá la memoria del objeto el tiempo que sea necesario para garantizar la ejecución correcta del método.

Pero esa explicación fue solo para completar. Como se dijo, no debe usar referencias débiles o suaves para objetos que no contengan recursos costosos.