threads thread practice example java concurrency

practice - thread java



Escenario de simultaneidad de Java: ¿necesito sincronización o no? (10)

Creo que es arriesgado Enhebrar resultados en todo tipo de problemas sutilmente que son un dolor gigante para depurar. Es posible que desee ver FastHashMap , que está destinado a casos de roscado de solo lectura como este.

Como mínimo, también declararé validProgramCodes como volatile para que la referencia no se optimice en un registro o algo así.

Aquí está el trato. Tengo un mapa hash que contiene datos que llamo "códigos de programa", que vive en un objeto, así:

Class Metadata { private HashMap validProgramCodes; public HashMap getValidProgramCodes() { return validProgramCodes; } public void setValidProgramCodes(HashMap h) { validProgramCodes = h; } }

Tengo montones y montones de hilos de lectura, cada uno de los cuales llamará a getValidProgramCodes () una vez y luego usará hashmap como un recurso de solo lectura.

Hasta aquí todo bien. Aquí es donde nos volvemos interesantes.

Quiero poner un temporizador que de vez en cuando genera una nueva lista de códigos de programa válidos (no importa cómo) y llama a setValidProgramCodes.

Mi teoría, que necesito ayuda para validar, es que puedo continuar usando el código como está, sin poner en sincronización explícita. Funciona así: en el momento en que validProgramCodes se actualizan, el valor de validProgramCodes siempre es bueno: es un puntero al hashmap nuevo o al antiguo. Esta es la suposición sobre la cual todo gira. Un lector que tiene el viejo hashmap está bien; él puede continuar usando el valor anterior, ya que no será basura recolectada hasta que lo libere. Cada lector es transitorio; morirá pronto y será reemplazado por uno nuevo que recogerá el nuevo valor.

¿Esto retiene el agua? Mi objetivo principal es evitar costosas sincronizaciones y bloqueos en la abrumadora mayoría de los casos donde no hay actualizaciones. Solo actualizamos una vez por hora más o menos, y los lectores parpadean constantemente.


Creo que tus suposiciones son correctas. Lo único que haría es establecer los validProgramCodes volátiles.

private volatile HashMap validProgramCodes;

De esta forma, cuando actualiza el "puntero" de validProgramCodes , garantiza que todos los subprocesos accedan al mismo "puntero" de HasMap porque no dependen de la memoria caché local de subprocesos y van directamente a la memoria.


La tarea funcionará siempre y cuando no te preocupe la lectura de valores obsoletos, y siempre que puedas garantizar que tu hashmap esté correctamente poblado en la inicialización. Al menos debe crear el hashMap con Collections.unmodifiableMap en el Hashmap para garantizar que sus lectores no cambien / eliminen objetos del mapa, y para evitar que varios hilos se pisen el uno sobre el otro e invaliden los iteradores cuando otros hilos se destruyen.

(El escritor anterior está en lo cierto acerca de la volatilidad, debería haber visto eso)


No, por el Modelo de memoria de Java (JMM), esto no es seguro para subprocesos.

No hay ninguna relación de casualidad entre la escritura y la lectura de los objetos de implementación de HashMap . Entonces, aunque el hilo del escritor parece escribir el objeto primero y luego la referencia, un hilo lector puede no ver el mismo orden.

Como también se mencionó, no hay garantía de que el hilo reaer verá el nuevo valor. En la práctica con compiladores actuales en hardware existente, el valor debe actualizarse, a menos que el cuerpo del bucle sea lo suficientemente pequeño como para que pueda ser lo suficientemente alineado.

Entonces, hacer la referencia volatile es adecuada bajo el nuevo JMM. Es poco probable que haga una diferencia sustancial en el rendimiento del sistema.

La moraleja de esta historia: enhebrar es difícil. No intentes ser inteligente, porque a veces (puede que no esté en tu sistema de prueba) no serás lo suficientemente inteligente.


Como otros ya han notado, esto no es seguro y no deberías hacer esto. Necesitas volátil o sincronizado aquí para forzar a otros hilos a ver el cambio.

Lo que no se ha mencionado es que sincronizados y especialmente volátiles son probablemente mucho más rápidos de lo que piensas. Si en realidad es un cuello de botella de rendimiento en su aplicación, entonces comeré esta página web.

Otra opción (probablemente más lenta que la volátil, pero YMMV) es usar ReentrantReadWriteLock para proteger el acceso, de manera que múltiples lectores simultáneos puedan leerlo. Y si eso sigue siendo un cuello de botella de rendimiento, comeré todo este sitio web.

public class Metadata { private HashMap validProgramCodes; private ReadWriteLock lock = new ReentrantReadWriteLock(); public HashMap getValidProgramCodes() { lock.readLock().lock(); try { return validProgramCodes; } finally { lock.readLock().unlock(); } } public void setValidProgramCodes(HashMap h) { lock.writeLock().lock(); try { validProgramCodes = h; } finally { lock.writeLock().unlock(); } } }


No, el ejemplo de código no es seguro, porque no hay publicación segura de ninguna instancia nueva de HashMap. Sin ninguna sincronización, existe la posibilidad de que un hilo lector vea un HashMap parcialmente inicializado .

Consulte la explicación de @ erickson en "Reordenar" en su respuesta. ¡Tampoco puedo recomendar el libro de Brian Goetz Java Concurrency in Practice suficiente!

Si está bien que los hilos del lector vean referencias HashMap viejas (inactivas), o incluso que nunca vean una nueva referencia, está al lado del punto. Lo peor que puede pasar es que un hilo lector pueda obtener referencias e intentar acceder a una instancia de HashMap que aún no se haya inicializado y no esté lista para acceder.


Si bien esta no es la mejor solución para este problema en particular (la idea de Erickson de una nueva matriz inmodificable es), me gustaría tomar un momento para mencionar la clase java.util.concurrent.ConcurrentHashMap introducida en Java 5, una versión de HashMap específicamente construido con concurrencia en mente. Esta construcción no bloquea las lecturas.


Usa Volatile

¿Es este un caso donde a un hilo le importa lo que otro está haciendo? Entonces, las preguntas frecuentes de JMM tienen la respuesta:

La mayoría de las veces, a un hilo no le importa lo que el otro está haciendo. Pero cuando lo hace, para eso sirve la sincronización.

En respuesta a aquellos que dicen que el código del OP es seguro tal como está, considere esto: no hay nada en el modelo de memoria de Java que garantice que este campo se vacíe a la memoria principal cuando se inicia un nuevo hilo. Además, una JVM es libre de reordenar las operaciones siempre que los cambios no sean detectables dentro del hilo.

En teoría, los hilos del lector no garantizan la "escritura" en validProgramCodes. En la práctica, eventualmente lo harán, pero no puedes estar seguro de cuándo.

Recomiendo declarar el miembro validProgramCodes como "volátil". La diferencia de velocidad será insignificante y garantizará la seguridad de su código ahora y en el futuro, independientemente de las optimizaciones de JVM que se puedan introducir.

Aquí hay una recomendación concreta:

import java.util.Collections; class Metadata { private volatile Map validProgramCodes = Collections.emptyMap(); public Map getValidProgramCodes() { return validProgramCodes; } public void setValidProgramCodes(Map h) { if (h == null) throw new NullPointerException("validProgramCodes == null"); validProgramCodes = Collections.unmodifiableMap(new HashMap(h)); } }

Inmutabilidad

Además de envolverlo con unmodifiableMap no unmodifiableMap , estoy copiando el mapa ( new HashMap(h) ). Esto hace que una instantánea no cambie incluso si la persona que llama del setter continúa actualizando el mapa "h". Por ejemplo, pueden borrar el mapa y agregar nuevas entradas.

Depende de las interfaces

En una nota estilística, a menudo es mejor declarar API con tipos abstractos como List y Map , en lugar de tipos concretos como ArrayList y HashMap. Esto proporciona flexibilidad en el futuro si los tipos de concreto necesitan cambiar (como lo hice aquí).

Almacenamiento en caché

El resultado de asignar "h" a "validProgramCodes" puede ser simplemente una escritura en el caché del procesador. Incluso cuando se inicia un nuevo subproceso, "h" no será visible para un nuevo subproceso a menos que se haya descargado a la memoria compartida. Un buen tiempo de ejecución evitará el enjuague a menos que sea necesario, y el uso de volatile es una forma de indicar que es necesario.

Reordenando

Supongamos el siguiente código:

HashMap codes = new HashMap(); codes.putAll(source); meta.setValidProgramCodes(codes);

Si setValidCodes es simplemente el validProgramCodes = h; de OP validProgramCodes = h; , el compilador puede reordenar el código de la siguiente manera:

1: meta.validProgramCodes = codes = new HashMap(); 2: codes.putAll(source);

Supongamos que después de la ejecución de la línea de escritura 1, un hilo lector comienza a ejecutar este código:

1: Map codes = meta.getValidProgramCodes(); 2: Iterator i = codes.entrySet().iterator(); 3: while (i.hasNext()) { 4: Map.Entry e = (Map.Entry) i.next(); 5: // Do something with e. 6: }

Supongamos ahora que el hilo del escritor llama "putAll" en el mapa entre la línea 2 y la línea 3 del lector. El mapa subyacente al Iterator experimentó una modificación concurrente y arroja una excepción en tiempo de ejecución, una excepción diabólicamente intermitente y aparentemente inexplicable que nunca fue ejecutada. producido durante la prueba.

Programación concurrente

Cada vez que tiene un hilo que se preocupa por lo que está haciendo otro hilo, debe tener algún tipo de barrera de memoria para garantizar que las acciones de un hilo sean visibles para el otro. Si un evento en un hilo debe ocurrir antes de un evento en otro hilo, debe indicarlo explícitamente. No hay garantías de lo contrario. En la práctica, esto significa volatile o synchronized .

No escatimes No importa qué tan rápido un programa incorrecto deje de hacer su trabajo. Los ejemplos que se muestran aquí son simples y artificiales, pero tenga la seguridad de que ilustran errores de concurrencia en el mundo real que son increíblemente difíciles de identificar y resolver debido a su imprevisibilidad y sensibilidad a la plataforma.

Recursos adicionales



Si leo el JLS correctamente (¡no hay garantías allí!), Los accesos a las referencias son siempre atómicos, punto. Ver la Sección 17.7 Tratamiento no atómico de doble y largo

Por lo tanto, si el acceso a una referencia es siempre atómico y no importa qué instancia del Hashmap devuelto Hashmap los hilos, debe estar bien. No verá escrituras parciales en la referencia, nunca.

Editar: Después de revisar la discusión en los comentarios a continuación y otras respuestas, aquí hay referencias / citas de

El libro de Doug Lea (Programación concurrente en Java, 2da edición), p 94, sección 2.2.7.2 Visibilidad , ítem n. ° 3: "

La primera vez que un subproceso accede a un campo de un objeto, ve el valor inicial del campo o el valor desde que lo escribió otro subproceso. "

En P. 94, Lea continúa describiendo los riesgos asociados con este enfoque:

El modelo de memoria garantiza que, dada la eventual ocurrencia de las operaciones anteriores, una actualización particular de un campo particular hecha por un subproceso será eventualmente visible para otro. Pero eventualmente puede ser un tiempo arbitrariamente largo.

Entonces, cuando absolutamente, de manera positiva, debe ser visible para cualquier cadena de llamada, se requiere una barrera de sincronización volatile o de otro tipo, especialmente en los subprocesos de larga ejecución o subprocesos que acceden al valor en un bucle (como dice Lea).

Sin embargo , en el caso de que exista un hilo de corta duración , como lo implica la pregunta, con nuevos hilos para lectores nuevos y que no afecte a la aplicación para leer datos obsoletos, no se requiere sincronización .

La respuesta de @erickson es la más segura en esta situación, garantizando que otros hilos verán los cambios en la referencia de HashMap medida que ocurran. Sugiero seguir ese consejo simplemente para evitar la confusión sobre los requisitos y la implementación que resultó en los "votos a la baja" en esta respuesta y en la discusión a continuación.

No borro la respuesta con la esperanza de que sea útil. No estoy buscando la insignia de "Presión de compañeros" ... ;-)