studio programacion móviles juegos interfaz hilos grafica desarrollo curso con aplicaciones java multithreading synchronization final

java - programacion - ¿Son los campos finales realmente útiles con respecto a la seguridad de los hilos?



juegos con hilos en java (4)

He estado trabajando diariamente con el modelo de memoria de Java desde hace algunos años. Creo que tengo un buen conocimiento del concepto de las carreras de datos y las diferentes formas de evitarlos (por ejemplo, bloques sincronizados, variables volátiles, etc.). Sin embargo, todavía hay algo que no creo que entienda completamente sobre el modelo de memoria, que es la forma en que se supone que los campos finales de las clases son seguros para subprocesos sin ninguna sincronización adicional.

Entonces, de acuerdo con la especificación, si un objeto está correctamente inicializado (es decir, no se escapa ninguna referencia al objeto en su constructor de tal manera que la referencia pueda ser vista por otro hilo), luego, después de la construcción, cualquier hilo que vea el se garantizará que el objeto vea las referencias a todos los campos finales del objeto (en el estado en que se encontraban cuando se construyó), sin ninguna sincronización adicional.

En particular, el estándar ( http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4 ) dice:

El modelo de uso para los campos finales es simple: establece los campos finales para un objeto en el constructor de ese objeto; y no escriba una referencia al objeto que se está construyendo en un lugar donde otro hilo pueda verlo antes de que el constructor del objeto finalice. Si se sigue esto, entonces, cuando el objeto es visto por otro hilo, ese hilo siempre verá la versión correctamente construida de los campos finales de ese objeto. También verá versiones de cualquier objeto o matriz a la que hagan referencia aquellos campos finales que estén al menos tan actualizados como los campos finales.

Incluso dan el siguiente ejemplo:

class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null) { int i = f.x; // guaranteed to see 3 int j = f.y; // could see 0 } } }

En el que se supone que un subproceso A ejecuta "reader ()", y un subproceso B debe ejecutar "writer ()".

Hasta ahora, todo bien, al parecer.

Mi principal preocupación tiene que ver con ... ¿es esto realmente útil en la práctica? Por lo que sé, para hacer que el subproceso A (que está ejecutando "reader ()") vea la referencia a "f", debemos usar algún mecanismo de sincronización, como hacer f volátil, o usar bloqueos para sincronizar el acceso a F. Si no lo hacemos, ni siquiera tenemos la garantía de que "reader ()" podrá ver una "f" inicializada, es decir, dado que no hemos sincronizado el acceso a "f", el lector potencialmente verá " nulo "en lugar del objeto que fue construido por el hilo del escritor. Este problema se describe en http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalWrong , que es una de las principales referencias para el modelo de memoria de Java ]:

Ahora, dicho todo esto, si, después de que un hilo construye un objeto inmutable (es decir, un objeto que solo contiene campos finales), desea asegurarse de que todos los demás hilos lo vean correctamente, por lo general todavía necesita utilizar la sincronización. No hay otra manera de asegurar, por ejemplo, que la segunda secuencia pueda ver la referencia al objeto inmutable . Las garantías que el programa obtiene de los campos finales se deben templar cuidadosamente con una comprensión profunda y cuidadosa de cómo se maneja la concurrencia en su código.

Entonces, si ni siquiera tenemos la garantía de ver la referencia a "f" y, por lo tanto, debemos usar los mecanismos de sincronización típicos (volátiles, bloqueos, etc.), y estos mecanismos ya hacen que las carreras de datos desaparezcan, la necesidad de la final es algo que ni siquiera consideraría. Quiero decir, si para hacer visible "f" a otros subprocesos todavía necesitamos usar bloques volátiles o sincronizados, y ya hacen que los campos internos sean visibles para los otros subprocesos ... ¿cuál es el punto (en términos de seguridad de subprocesos) en ¿Haciendo una final de campo en primer lugar?


Creo que está malinterpretando lo que pretende mostrar el ejemplo de JLS:

static void reader() { if (f != null) { int i = f.x; // guaranteed to see 3 int j = f.y; // could see 0 } }

Este código no garantiza que el hilo que llama al reader() vea el último valor de f . Pero lo que está diciendo es que si ve f como no nulo, entonces fx tiene la garantía de ser 3 ... a pesar del hecho de que en realidad no hicimos ninguna sincronización explícita.

¿Es útil esta sincronización implícita para finales en constructores? Ciertamente es ... OMI. Esto significa que no necesitamos realizar ninguna sincronización adicional cada vez que accedemos al estado de un objeto inmutable. Eso es algo bueno, ya que la sincronización normalmente implica la lectura o el almacenamiento en caché, y eso ralentiza el programa.

Pero lo que Pugh está diciendo es que, en primer lugar, tendrá que sincronizar para obtener la referencia al objeto inmutable. Está señalando que el uso de objetos inmutables (implementado con final ) no le exime de la necesidad de sincronizar ... o de la necesidad de comprender la implementación de la concurrencia / sincronización de su aplicación.

El problema es que todavía debemos asegurarnos de que el lector verá una "f" que no sea nula, y eso solo es posible si usamos otro mecanismo de sincronización que ya proporcione la semántica de permitirnos ver 3 para fx Y si ese es el caso, ¿por qué molestarse en usar final para cosas de seguridad de hilo?

Hay una diferencia entre la sincronización para obtener la referencia y la sincronización para utilizar la referencia. El primero que deba hacer solo una vez. El segundo es posible que tenga que hacer muchas veces ... con la misma referencia. E incluso si es uno a uno, todavía he reducido a la mitad el número de operaciones de sincronización ... si yo (hipotéticamente) implemento el objeto inmutable como seguro para subprocesos.


Sí, los campos finales finales son útiles en términos de seguridad de subprocesos. Puede que no sea útil en su ejemplo, sin embargo, si observa la implementación anterior de ConcurrentHashMap , el método get no aplica ningún bloqueo mientras busca el valor, aunque existe el riesgo de que mientras se realiza la búsqueda la lista podría cambiar ( pensar en ConcurrentModificationException). Sin embargo, CHM usa la lista hecha de final archivado para el campo ''siguiente'' que garantiza la consistencia de la lista (los elementos en el frente / aún no se verán aumentarán o disminuirán). Así que la ventaja es que la seguridad de subprocesos se establece sin sincronización.

Del articulo

Explotando la inmutabilidad

Se evita una fuente importante de inconsistencia al hacer que los elementos de entrada sean casi inmutables: todos los campos son finales, excepto el campo de valor, que es volátil. Esto significa que los elementos no pueden agregarse o eliminarse desde la mitad o al final de la cadena hash; los elementos solo se pueden agregar al principio, y la eliminación implica la clonación total o parcial de la cadena y la actualización del puntero del encabezado de la lista. Entonces, una vez que tenga una referencia en una cadena de hash, aunque no sepa si tiene una referencia al encabezado de la lista, sí sabe que el resto de la lista no cambiará su estructura. Además, dado que el campo de valor es volátil, podrá ver las actualizaciones al campo de valor de inmediato, lo que simplificará en gran medida el proceso de escritura de una implementación de Mapa que puede lidiar con una vista de memoria potencialmente obsoleta.

Mientras que el nuevo JMM proporciona seguridad de inicialización para las variables finales, el JMM antiguo no lo hace, lo que significa que es posible que otro hilo vea el valor predeterminado para un campo final, en lugar del valor colocado allí por el constructor del objeto. La implementación debe estar preparada para detectar esto también, lo que hace al garantizar que el valor predeterminado para cada campo de Entrada no sea un valor válido. La lista se construye de tal manera que si alguno de los campos de Entrada parece tener su valor predeterminado (cero o nulo), la búsqueda fallará, lo que provocará que la implementación de get () se sincronice y atraviese la cadena nuevamente.

Enlace del artículo: https://www.ibm.com/developerworks/library/j-jtp08223/


Tiene razón, dado que el bloqueo ofrece mayores garantías, la garantía sobre la disponibilidad de los final no es particularmente útil en presencia del bloqueo. Sin embargo, el bloqueo no siempre es necesario para garantizar un acceso concurrente confiable.

Por lo que sé, para hacer que el subproceso A (que está ejecutando "reader ()") vea la referencia a "f", debemos usar algún mecanismo de sincronización, como hacer f volátil, o usar bloqueos para sincronizar el acceso a F.

Hacer f volátil no es un mecanismo de sincronización; obliga a los hilos a leer la memoria cada vez que se accede a la variable, pero no sincroniza el acceso a una ubicación de la memoria. El bloqueo es una forma de sincronizar el acceso, pero en la práctica no es necesario garantizar que los dos subprocesos compartan datos de manera confiable. Por ejemplo, podría usar una clase ConcurrentLinkedQueue<E> , que es una colección concurrente sin bloqueo * , para pasar los datos de un hilo lector a un hilo escritor y evitar la sincronización. También puede usar AtomicReference<T> para garantizar un acceso simultáneo confiable a un objeto sin bloqueo.

Cuando se utiliza la concurrencia sin bloqueo, la garantía sobre la visibilidad de los campos final resulta útil. Si realiza una colección sin bloqueo y la utiliza para almacenar objetos inmutables, sus hilos podrán acceder al contenido de los objetos sin un bloqueo adicional.

* ConcurrentLinkedQueue<E> no solo está libre de bloqueo, sino que también es una colección sin espera (es decir, una colección sin bloqueo con garantías adicionales que no son relevantes para esta discusión).


TL; DR: La mayoría de los desarrolladores de software deben ignorar las reglas especiales con respecto a las variables finales en el modelo de memoria Java . Deberían cumplir con la regla general: si un programa está libre de carreras de datos , todas las ejecuciones parecerán ser consistentemente secuenciales . En la mayoría de los casos, las variables finales no se pueden usar para mejorar el rendimiento del código concurrente, porque la regla especial en el Modelo de Memoria Java crea algunos costos adicionales para las variables finales , lo que hace que las variables volátiles sean superiores a las finales para casi todos los casos de uso.

La regla especial sobre las variables finales impide, en algunos casos, que una variable final pueda mostrar valores diferentes. Sin embargo, en términos de rendimiento, la regla es irrelevante.

Dicho esto, aquí hay una respuesta más detallada. Pero tengo que avisarte. La siguiente descripción puede contener cierta información precaria, que la mayoría de los desarrolladores de software nunca deberían preocuparse, y es mejor si no la conocen.

La regla especial sobre las variables finales en el modelo de memoria Java implica de alguna manera que hace una diferencia para el compilador Java VM y JIT de Java, si una variable miembro es final o no lo es.

public class Int { public /* final */ int value; public Int(int value) { this.value = value; } }

Si echa un vistazo al código fuente de Hotspot , verá que el compilador comprueba si el constructor de una clase escribe al menos una variable final . Si lo hace, el compilador emitirá código adicional para el constructor, más precisamente una barrera de liberación de memoria . También encontrarás el siguiente comentario en el código fuente:

Este método (que debe ser un constructor según las reglas de Java) escribió un final. Los efectos de todas las inicializaciones deben confirmarse en la memoria antes de cualquier código después de que el constructor publique la referencia al nuevo objeto constructor. En lugar de esperar por la publicación, simplemente bloqueamos las escrituras aquí. En lugar de poner una barrera en solo aquellas escrituras que se requieren completar, forzamos que todas las escrituras se completen.

Eso significa que la inicialización de una variable final es similar a una escritura de una variable volátil . Implica algún tipo de barrera de liberación de memoria . Sin embargo, como puede verse en el comentario citado, las variables finales pueden ser incluso más caras. Y lo que es peor, tiene estos costos adicionales para las variables finales independientemente de si se usan en código concurrente o no.

Eso es horrible, porque queremos que los desarrolladores de software utilicen las variables finales para aumentar la legibilidad y la capacidad de mantenimiento del código fuente. Desafortunadamente, el uso de variables finales puede afectar significativamente el rendimiento de un programa.

La pregunta sigue siendo: ¿Existen casos de uso en los que la regla especial con respecto a las variables finales ayude a mejorar el rendimiento del código concurrente ?

Es difícil decirlo, porque depende de la implementación real de la máquina virtual de Java y de la arquitectura de memoria de la máquina. No he visto ningún caso de uso hasta ahora. Una rápida mirada al código fuente del paquete java.util.concurrent tampoco reveló nada.

El problema es: la inicialización de una variable final es tan costosa como la escritura de una variable volátil o atómica . Si usa una variable volátil para la referencia del objeto recién creado, obtendrá el mismo comportamiento y los costos, con la excepción de que la referencia también se publicará de inmediato. Entonces, básicamente no hay beneficio en el uso de variables finales para la programación concurrente.