Esta es una especie de encuesta sobre problemas comunes de concurrencia en Java. Un ejemplo podría ser el clásico punto muerto o la condición de carrera o quizás los errores de subprocesamiento EDT en Swing. Estoy interesado tanto en una variedad de posibles problemas como en qué problemas son más comunes. Por lo tanto, deje una respuesta específica de un error de concurrencia de Java por comentario y vote si ve uno que haya encontrado.
java
multithreading
concurrency
Alex Miller
fuente
fuente
Respuestas:
El problema de concurrencia más común que he visto es no darse cuenta de que un campo escrito por un hilo no garantiza que sea visto por un hilo diferente. Una aplicación común de esto:
Mientras parada no es volátil o
setStop
yrun
no están sincronizados esto no se garantiza que funcione. Este error es especialmente diabólico ya que en el 99.999% no importará en la práctica ya que el hilo del lector eventualmente verá el cambio, pero no sabemos qué tan pronto lo vio.fuente
Mi problema de concurrencia más doloroso número 1 ocurrió cuando dos bibliotecas de código abierto diferentes hicieron algo como esto:
A primera vista, esto parece un ejemplo de sincronización bastante trivial. Sin embargo; Debido a que las cadenas están internados en Java, la cadena literal
"LOCK"
resulta ser la misma instancia dejava.lang.String
(a pesar de que se declaran completamente diferentes entre sí). El resultado es obviamente malo.fuente
Un problema clásico es cambiar el objeto en el que está sincronizando mientras lo sincroniza:
Otros hilos concurrentes se sincronizan en un objeto diferente y este bloque no proporciona la exclusión mutua que espera.
fuente
Un problema común es usar clases como Calendar y SimpleDateFormat de varios subprocesos (a menudo cachéándolos en una variable estática) sin sincronización. Estas clases no son seguras para subprocesos, por lo que el acceso de subprocesos múltiples en última instancia causará problemas extraños con un estado inconsistente.
fuente
Bloqueo de doble verificación. En general.
El paradigma, del cual comencé a aprender los problemas cuando trabajaba en BEA, es que la gente verificará un singleton de la siguiente manera:
Esto nunca funciona, porque otro hilo podría haber entrado en el bloque sincronizado y s_instance ya no es nulo. Entonces, el cambio natural es hacerlo:
Eso tampoco funciona, porque el modelo de memoria Java no lo admite. Debe declarar s_instance como volátil para que funcione, e incluso entonces solo funciona en Java 5.
Las personas que no están familiarizadas con las complejidades del modelo de memoria Java lo estropean todo el tiempo .
fuente
No se sincroniza correctamente en los objetos devueltos por
Collections.synchronizedXXX()
, especialmente durante la iteración o las operaciones múltiples:Eso esta mal . A pesar de las operaciones individuales
synchronized
, el estado del mapa entre la invocacióncontains
yput
puede ser cambiado por otro hilo. Debería ser:O con una
ConcurrentMap
implementación:fuente
Aunque probablemente no sea exactamente lo que está pidiendo, el problema más frecuente relacionado con la concurrencia que he encontrado (probablemente porque aparece en el código normal de un solo subproceso) es un
java.util.ConcurrentModificationException
causado por cosas como:
fuente
Puede ser fácil pensar que las colecciones sincronizadas le brindan más protección de la que realmente ofrecen, y se olvida de mantener el bloqueo entre llamadas. He visto este error varias veces:
Por ejemplo, en la segunda línea anterior, los métodos
toArray()
ysize()
son seguros para subprocesos por derecho propio, perosize()
se evalúa por separado detoArray()
, y el bloqueo en la Lista no se mantiene entre estas dos llamadas.Si ejecuta este código con otro subproceso que elimina simultáneamente elementos de la lista, tarde o temprano terminará con un nuevo
String[]
retorno que es más grande de lo necesario para mantener todos los elementos en la lista y tiene valores nulos en la cola. Es fácil pensar que debido a que las dos llamadas de método a la Lista ocurren en una sola línea de código, esto es de alguna manera una operación atómica, pero no lo es.fuente
El error más común que vemos donde trabajo es que los programadores realizan operaciones largas, como llamadas al servidor, en el EDT, bloquean la GUI durante unos segundos y hacen que la aplicación no responda.
fuente
Olvidarse de esperar () (o Condition.await ()) en un bucle, verificando que la condición de espera sea realmente verdadera. Sin esto, te encuentras con errores de espurias wait () wakeups. El uso canónico debe ser:
fuente
Otro error común es la mala gestión de excepciones. Cuando un subproceso en segundo plano genera una excepción, si no lo maneja correctamente, es posible que no vea el seguimiento de la pila. O tal vez su tarea en segundo plano deja de ejecutarse y nunca comienza de nuevo porque no pudo manejar la excepción.
fuente
Hasta que tomó una clase con Brian Goetz que no se dio cuenta de que la no sincronizada
getter
de un campo privado mutado a través de un sincronizadosetter
se Nunca garantizado para devolver el valor actualizado. Solo cuando una variable esté protegida por un bloque sincronizado en ambas lecturas Y escrituras obtendrá la garantía del último valor de la variable.fuente
Pensando que está escribiendo código de subproceso único, pero utilizando estadísticas estáticas mutables (incluidos los únicos) Obviamente serán compartidos entre hilos. Esto sucede sorprendentemente a menudo.
fuente
Las llamadas a métodos arbitrarios no deben realizarse desde bloques sincronizados.
Dave Ray tocó esto en su primera respuesta, y de hecho también encontré un punto muerto que también tenía que ver con invocar métodos en los oyentes desde un método sincronizado. Creo que la lección más general es que las llamadas a métodos no deben realizarse "en la naturaleza" desde un bloque sincronizado: no tiene idea de si la llamada será de larga duración, dará como resultado un punto muerto o lo que sea.
En este caso, y generalmente en general, la solución fue reducir el alcance del bloque sincronizado para proteger solo un elemento privado crítico sección de código.
Además, dado que ahora estábamos accediendo a la Colección de oyentes fuera de un bloque sincronizado, la cambiamos para que sea una Colección de copia en escritura. O podríamos simplemente haber hecho una copia defensiva de la Colección. El punto es que generalmente hay alternativas para acceder de forma segura a una Colección de objetos desconocidos.
fuente
El error más reciente relacionado con la concurrencia con el que me encontré fue un objeto que en su constructor creó un ExecutorService, pero cuando el objeto ya no estaba referenciado, nunca había apagado el ExecutorService. Por lo tanto, durante un período de semanas, filtraron miles de subprocesos, lo que eventualmente provocó el bloqueo del sistema. (Técnicamente, no se bloqueó, pero dejó de funcionar correctamente, mientras continuaba ejecutándose).
Técnicamente, supongo que esto no es un problema de concurrencia, pero es un problema relacionado con el uso de las bibliotecas java.util.concurrency.
fuente
La sincronización desequilibrada, particularmente contra Maps, parece ser un problema bastante común. Muchas personas creen que la sincronización de los puestos en un mapa (no un mapa concurrente, pero digamos un HashMap) y no sincronizar en los get es suficiente. Sin embargo, esto puede conducir a un bucle infinito durante el nuevo hash.
Sin embargo, el mismo problema (sincronización parcial) puede ocurrir en cualquier lugar donde haya compartido estado con lecturas y escrituras.
fuente
Encontré un problema de concurrencia con Servlets, cuando hay campos mutables que se establecerán en cada solicitud. Pero solo hay una instancia de servlet para todas las solicitudes, por lo que funcionó perfectamente en un solo entorno de usuario, pero cuando más de un usuario solicitó el servlet, se produjeron resultados impredecibles.
fuente
No es exactamente un error, pero el peor pecado es proporcionar una biblioteca que desea que otras personas usen, pero no indicar qué clases / métodos son seguros para subprocesos y cuáles solo deben llamarse desde un solo subproceso, etc.
Más personas deberían hacer uso de las anotaciones de concurrencia (por ejemplo, @ThreadSafe, @GuardedBy, etc.) descritas en el libro de Goetz.
fuente
Mi mayor problema siempre han sido los puntos muertos, especialmente causados por los oyentes que se activan con un bloqueo. En estos casos, es realmente fácil invertir el bloqueo entre dos hilos. En mi caso, entre una simulación que se ejecuta en un subproceso y una visualización de la simulación que se ejecuta en el subproceso de la interfaz de usuario.
EDITAR: Se movió la segunda parte para separar la respuesta.
fuente
Iniciar un hilo dentro del constructor de una clase es problemático. Si la clase se extiende, el subproceso se puede iniciar antes de que se ejecute el constructor de la subclase .
fuente
Clases mutables en estructuras de datos compartidas.
Cuando esto sucede, el código es mucho más complejo que este ejemplo simplificado. Replicar, encontrar y corregir el error es difícil. Quizás podría evitarse si pudiéramos marcar ciertas clases como inmutables y ciertas estructuras de datos como que solo contienen objetos inmutables.
fuente
La sincronización en un literal de cadena o constante definida por un literal de cadena es (potencialmente) un problema ya que el literal de cadena está internado y será compartido por cualquier otra persona en la JVM que use el mismo literal de cadena. Sé que este problema ha surgido en servidores de aplicaciones y otros escenarios de "contenedor".
Ejemplo:
En este caso, cualquiera que use la cadena "foo" para bloquear está compartiendo el mismo bloqueo.
fuente
Creo que en el futuro el principal problema con Java será la (falta de) garantías de visibilidad para los constructores. Por ejemplo, si crea la siguiente clase
y luego simplemente lea la propiedad de MyClass a desde otro hilo, MyClass.a podría ser 0 o 1, dependiendo de la implementación y el estado de ánimo de JavaVM. Hoy las posibilidades de que 'a' sea 1 son muy altas. Pero en futuras máquinas NUMA esto puede ser diferente. Muchas personas no son conscientes de esto y creen que no necesitan preocuparse por los subprocesos múltiples durante la fase de inicialización.
fuente
El error más tonto que frecuentemente hago es olvidar sincronizar antes de llamar a notify () o wait () en un objeto.
fuente
Usando un "nuevo Object ()" local como mutex.
Esto es inútil
fuente
Otro problema común de 'concurrencia' es usar código sincronizado cuando no es necesario en absoluto. Por ejemplo, todavía veo programadores que usan
StringBuffer
o inclusojava.util.Vector
(como variables locales del método).fuente
Múltiples objetos que están protegidos por bloqueo pero a los que comúnmente se accede sucesivamente. Nos hemos encontrado con un par de casos en los que los bloqueos se obtienen por diferentes códigos en diferentes órdenes, lo que resulta en un punto muerto.
fuente
No darse cuenta de que el
this
de una clase interna no es elthis
de la clase externa. Típicamente en una clase interna anónima que implementaRunnable
. El problema raíz es que porque la sincronización es parte de todoObject
correos electrónicos, efectivamente no hay verificación de tipo estático. Lo he visto al menos dos veces en Usenet, y también aparece en Brian Goetz'z Java Concurrency in Practice.Los cierres de BGGA no sufren de esto ya que no hay
this
para el cierre (hacethis
referencia a la clase externa). Si usa nothis
objetos como bloqueos, entonces se soluciona este problema y otros.fuente
Uso de un objeto global como una variable estática para el bloqueo.
Esto lleva a un rendimiento muy malo debido a la contención.
fuente
Honestamente? Antes del advenimiento
java.util.concurrent
, el problema más común con el que me topaba habitualmente era lo que yo llamo "intercambio de subprocesos": aplicaciones que usan subprocesos para concurrencia, pero generan demasiados y terminan agitándose.fuente