java multithreading volatile final

Java: ¿Todos los campos son finales o volátiles?



multithreading volatile (3)

Si tengo un objeto que se comparte entre subprocesos, me parece que cada campo debe ser final o volatile , con el siguiente razonamiento:

  • si el campo debe cambiarse (señalar a otro objeto, actualizar el valor primitivo), entonces el campo debe ser volatile para que todos los otros subprocesos operen en el nuevo valor. Simplemente una sincronización en los métodos que acceden a dicho campo es insuficiente porque pueden devolver un valor almacenado en caché.

  • Si el campo nunca cambia, hágalo final .

Sin embargo, no pude encontrar nada sobre esto, así que me pregunto si esta lógica es defectuosa o demasiado obvia.

EDIT, por supuesto, en lugar de volátil, se podría usar una final AtomicReference o similar.

EDITAR, por ejemplo, ver ¿Es el método de obtención una alternativa a volátil en Java?

EDITAR para evitar confusiones: ¡ Esta pregunta es sobre la invalidación de caché! Si dos subprocesos operan en el mismo objeto, los campos de los objetos se pueden almacenar en caché (por subproceso), si no se declaran volátiles. ¿Cómo puedo garantizar que la memoria caché se invalida correctamente?

EDICIÓN FINAL Gracias a @Peter Lawrey, quien me señaló a JLS §17 (modelo de memoria Java). Por lo que veo, indica que la sincronización establece una relación de suceso antes de las operaciones, de modo que un subproceso ve las actualizaciones de otro subproceso si esas actualizaciones "sucedieron antes", por ejemplo, si el captador y el configurador para un campo no volátil son synchronized


Crear campos que no necesita cambiar de forma final es una buena idea, independientemente de las preocupaciones de subprocesos. Hace que las instancias de la clase sean más fáciles de razonar, porque puedes saber en qué estado se encuentra con mayor facilidad.

En términos de hacer volatile los otros campos:

Simplemente una sincronización en los métodos que acceden a dicho campo es insuficiente porque pueden devolver un valor almacenado en caché.

Solo vería un valor en caché si accede al valor fuera de un bloque sincronizado.

Todos los accesos deberían estar correctamente sincronizados. El final de un bloque sincronizado se garantiza antes del inicio de otro bloque sincronizado (cuando se sincroniza en el mismo monitor).

Hay al menos un par de casos en los que aún necesitarías usar la sincronización:

  • Desearía usar la sincronización si tuviera que leer y luego actualizar uno o más campos de forma atómica.
    • Es posible que pueda evitar la sincronización para ciertas actualizaciones de un solo campo, por ejemplo, si puede usar una clase Atomic* lugar de un "campo antiguo"; pero incluso para una actualización de un solo campo, aún podría requerir acceso exclusivo (por ejemplo, agregar un elemento a una lista mientras elimina otro).
  • Además, volatile / final puede ser insuficiente para valores seguros sin subprocesos, como un ArrayList o una matriz.

Si bien creo que private final probablemente debería haber sido el valor predeterminado para los campos y las variables con una palabra clave como var que la hace mutable, el uso de volatile cuando no la necesita es

  • mucho más lento, a menudo alrededor de 10x más lento.
  • Por lo general, no le brinda la seguridad de subprocesos que necesita, pero puede dificultar la búsqueda de estos errores al hacer que sean menos propensos a aparecer.
  • a diferencia de final que mejora la claridad al decir que esto no debe modificarse, usar volatile cuando no es necesario, es probable que sea confuso, ya que el lector intenta averiguar por qué se hizo volátil.

si el campo debe cambiarse (señalar a otro objeto, actualizar el valor primitivo), entonces el campo debe ser volátil para que todos los otros subprocesos operen en el nuevo valor.

Si bien esto está bien para las lecturas, considere este caso trivial.

volatile int x; x++;

Esto no es seguro para subprocesos. Como es lo mismo que

int x2 = x; x2 = x2 + 1; // multiple threads could be executing on the same value at this point. x = x2;

Lo que es peor es que usar volatile haría que este tipo de error sea más difícil de encontrar.

Como apunta yshavit, actualizar campos múltiples es más difícil de HashMap.put(a, b) con volatile por ejemplo, HashMap.put(a, b) actualiza varias referencias.

Simplemente una sincronización en los métodos que acceden a dicho campo es insuficiente porque pueden devolver un valor almacenado en caché.

sincronizado te da todas las garantías de memoria de volatile y más, por lo que es significativamente más lento.

NOTA: Solo synchronized todos los métodos tampoco es suficiente. StringBuffer tiene todos los métodos sincronizados, pero es peor que inútil en un contexto de subprocesos múltiples, ya que es probable que su uso sea propenso a errores.

Es demasiado fácil asumir que lograr la seguridad de los hilos es como rociar polvo de hadas, agregar algo de seguridad de hilos mágicos y tus insectos desaparecen. El problema es que la seguridad del hilo es más como un cubo con muchos agujeros. Tape los orificios más grandes y puede parecer que los errores desaparecen, pero a menos que los conecte todos, no tiene seguridad para los hilos, pero puede ser más difícil encontrarlos.

En términos de sincronización vs volátil, esto indica

Otros mecanismos, como las lecturas y escrituras de variables volátiles y el uso de clases en el paquete java.util.concurrent, proporcionan formas alternativas de sincronización.

https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html


Si un objeto se comparte entre subprocesos, tiene dos opciones claras:

1. Haz que el objeto sea de solo lectura.

Por lo tanto, las actualizaciones (o caché) no tienen ningún impacto.

2. Sincronizar en el propio objeto.

La invalidación de caché es difícil. Muy duro. Entonces, si no necesita garantizar valores obsoletos, debe proteger ese valor y proteger el bloqueo alrededor de dicho valor.

Haga que el bloqueo y los valores sean privados en un objeto compartido, por lo que las operaciones aquí son un detalle de implementación.

Para evitar bloqueos muertos, estas operaciones deben ser tan "atómicas", como para evitar interactuar con cualquier otro bloqueo.