¿Cuál es el problema de simultaneidad más frecuente que has encontrado en Java?

Esta es una encuesta de tipo sobre problemas comunes de simultaneidad en Java. Un ejemplo podría ser el estancamiento clásico o la condición de carrera o quizás EDT enhebrar errores en Swing. Me interesan tanto los problemas posibles como los problemas más comunes. Por lo tanto, deje una respuesta específica de un error de simultaneidad de Java por comentario y vota si ves uno que hayas encontrado.

El problema de concurrencia más común que he visto, no es darse cuenta de que un campo escrito por un hilo no está garantizado para ser visto por un hilo diferente. Una aplicación común de esto:

class MyThread extends Thread { private boolean stop = false; public void run() { while(!stop) { doSomeWork(); } } public void setStop() { this.stop = true; } } 

Mientras stop no sea volátil o setStop y run no estén sincronizados, no se garantiza que esto funcione. Este error es especialmente diabólico, ya que en 99.999% no importará en la práctica ya que el hilo del lector eventualmente verá el cambio, pero no sabemos cuán pronto lo vio.

Mi problema de concurrencia más doloroso # 1 se produjo cuando dos bibliotecas de código abierto diferentes hicieron algo como esto:

 private static final String LOCK = "LOCK"; // use matching strings // in two different libraries public doSomestuff() { synchronized(LOCK) { this.work(); } } 

A primera vista, esto parece un ejemplo de sincronización bastante trivial. Sin embargo; porque las cadenas están internados en Java, la cadena literal "LOCK" resulta ser la misma instancia de java.lang.String (a pesar de que se declaran completamente dispares entre sí). El resultado es obviamente malo.

Un problema clásico es cambiar el objeto que está sincronizando mientras se sincroniza en él:

 synchronized(foo) { foo = ... } 

Otros subprocesos simultáneos se sincronizan en un objeto diferente y este bloque no proporciona la exclusión mutua que espera.

Un problema común es utilizar clases como Calendar y SimpleDateFormat desde varios subprocesos (a menudo al almacenarlos en caché en una variable estática) sin sincronización. Estas clases no son seguras para subprocesos, por lo que el acceso de subprocesos múltiples ocasionará problemas extraños con un estado incoherente.

Bloqueo doblemente verificado. En general.

El paradigma, que empecé a conocer los problemas de cuando estaba trabajando en BEA, es que las personas verifican un singleton de la siguiente manera:

 public Class MySingleton { private static MySingleton s_instance; public static MySingleton getInstance() { if(s_instance == null) { synchronized(MySingleton.class) { s_instance = new MySingleton(); } } return s_instance; } } 

Esto nunca funciona, porque otro subproceso podría haber entrado en el bloque sincronizado y s_instance ya no es nulo. Entonces el cambio natural es hacerlo:

  public static MySingleton getInstance() { if(s_instance == null) { synchronized(MySingleton.class) { if(s_instance == null) s_instance = new MySingleton(); } } return s_instance; } 

Eso tampoco funciona, porque el Modelo de memoria Java no lo admite. Debe declarar s_instance como volátil para que funcione, y aun así solo funciona en Java 5.

Las personas que no están familiarizadas con las complejidades del Modelo de Memoria de Java lo estropean todo el tiempo .

Sin sincronizar correctamente en los objetos devueltos por Collections.synchronizedXXX() , especialmente durante la iteración o en varias operaciones:

 Map map = Collections.synchronizedMap(new HashMap()); ... if(!map.containsKey("foo")) map.put("foo", "bar"); 

Eso está mal . A pesar de que las operaciones individuales se synchronized , el estado del mapa entre invocar contains y put puede cambiarse por otro hilo. Debería ser:

 synchronized(map) { if(!map.containsKey("foo")) map.put("foo", "bar"); } 

O con una implementación de ConcurrentMap :

 map.putIfAbsent("foo", "bar"); 

Aunque probablemente no es 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 una

java.util.ConcurrentModificationException

causado por cosas como:

 List list = new ArrayList(Arrays.asList("a", "b", "c")); for (String string : list) { list.remove(string); } 

El error más común que vemos donde trabajo es que los progtwigdores realizan operaciones largas, como llamadas al servidor, en el EDT, bloqueando la GUI por unos segundos y haciendo que la aplicación no responda.

Olvidarse de esperar () (o Condition.await ()) en un bucle, verificando que la condición de espera sea verdadera. Sin esto, te encuentras con errores de wake-ups espúreos. El uso canónico debe ser:

  synchronized (obj) { while () { obj.wait(); } // do stuff based on condition being true } 

Otro error común es el mal manejo de excepciones. Cuando un hilo de fondo arroja una excepción, si no lo maneja correctamente, es posible que no vea el trazado de la stack en absoluto. O tal vez su tarea de fondo deje de ejecutarse y nunca vuelva a comenzar porque no pudo manejar la excepción.

Puede ser fácil pensar que las colecciones sincronizadas le otorgan más protección de la que realmente tienen, y se olvida de mantener el locking entre las llamadas. He visto este error algunas veces:

  List l = Collections.synchronizedList(new ArrayList()); String[] s = l.toArray(new String[l.size()]); 

Por ejemplo, en la segunda línea anterior, los toArray() y size() son seguros para la ejecución de subprocesos por sí mismos, pero el size() se evalúa por separado de toArray() y no se mantiene el locking en la lista entre estas dos llamadas.

Si ejecuta este código con otro hilo al mismo tiempo eliminando elementos de la lista, tarde o temprano terminará con un nuevo String[] devuelto que es más grande que el requerido para contener todos los elementos en la lista, y tiene valores nulos en la cola . Es fácil pensar que debido a que las dos llamadas a los métodos a la Lista se producen en una sola línea de código, esto de alguna manera es una operación atómica, pero no lo es.

Hasta que tomé una clase con Brian Goetz, no me di cuenta de que el getter no sincronizado de un campo privado mutado a través de un setter sincronizado nunca garantiza devolver el valor actualizado. Solo cuando una variable está protegida por un bloque sincronizado en ambas lecturas Y se obtendrá la garantía del último valor de la variable.

 public class SomeClass{ private Integer thing = 1; public synchronized void setThing(Integer thing) this.thing = thing; } /** * This may return 1 forever and ever no matter what is set * because the read is not synched */ public Integer getThing(){ return thing; } } 

Pensando que está escribiendo código de subproceso único, pero utilizando estática mutable (incluidos los singletons). Obviamente, se compartirán entre hilos. Esto sucede sorprendentemente a menudo.

Las llamadas a métodos arbitrarios no deben realizarse desde dentro de bloques sincronizados.

Dave Ray se refirió a esto en su primera respuesta, y de hecho también me encontré con un punto muerto que también tiene que ver con invocar métodos a los oyentes desde un método sincronizado. Creo que la lección más general es que las llamadas a métodos no deben hacerse “en la naturaleza” desde un bloque sincronizado; no tienes idea de si la llamada será de larga duración, dará como resultado un punto muerto o lo que sea.

En este caso, y por lo general en general, la solución fue reducir el scope del bloque sincronizado para solo proteger una sección privada crítica de código.

Además, dado que ahora estábamos accediendo a la colección de oyentes fuera de un bloque sincronizado, lo cambiamos por una colección de copia por 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.

Encontré un problema de concurrencia con Servlets, cuando hay campos mutables que serán configurados por cada solicitud. Pero solo hay una instancia de servlet para todas las solicitudes, por lo que funcionó perfectamente en un único entorno de usuario, pero cuando más de un usuario solicitó el servlet se produjeron resultados imprevisibles.

 public class MyServlet implements Servlet{ private Object something; public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException{ this.something = request.getAttribute("something"); doSomething(); } private void doSomething(){ this.something ... } } 

El error más reciente relacionado con Concurrency que encontré fue un objeto que en su constructor creó un ExecutorService, pero cuando el objeto ya no se referenciaba, nunca había apagado el ExecutorService. Por lo tanto, durante un período de semanas, miles de subprocesos se filtraron y, finalmente, el sistema se bloqueó. (Técnicamente, no se bloqueó, pero dejó de funcionar correctamente, mientras continuaba funcionando).

Técnicamente, supongo que esto no es un problema de concurrencia, pero es un problema relacionado con el uso de las bibliotecas de java.util.concurrency.

Mi mayor problema siempre han sido los lockings, especialmente causados ​​por oyentes que son despedidos con un locking. En estos casos, es muy fácil obtener el locking invertido entre dos hilos. En mi caso, entre una simulación que se ejecuta en un hilo y una visualización de la simulación que se ejecuta en el hilo de la interfaz de usuario.

EDITAR: movió la segunda parte para separar la respuesta.

La sincronización desbalanceada, particularmente contra Maps, parece ser un problema bastante común. Mucha gente cree que la sincronización en las puestas en un Mapa (no en una Simulación Concurrente, pero digamos un HashMap) y no sincronizarse en obtener es suficiente. Sin embargo, esto puede conducir a un bucle infinito durante re-hash.

El mismo problema (sincronización parcial) puede ocurrir en cualquier lugar que haya compartido estado con lecturas y escrituras sin embargo.

No es exactamente un error, pero el peor pecado es proporcionar una biblioteca que pretendas que otras personas utilicen, pero sin indicar qué clases / métodos son seguros para la ejecución de subprocesos y cuáles se deben invocar solo desde un solo subproceso, etc.

Más personas deberían usar las anotaciones de simultaneidad (por ejemplo, @ThreadSafe, @GuardedBy, etc.) descritas en el libro de Goetz.

Iniciar un hilo dentro del constructor de una clase es problemático. Si la clase se extiende, el hilo se puede iniciar antes de que se ejecute el constructor de la subclase .

Clases mutables en estructuras de datos compartidas

 Thread1: Person p = new Person("John"); sharedMap.put("Key", p); assert(p.getName().equals("John"); // sometimes passes, sometimes fails Thread2: Person p = sharedMap.get("Key"); p.setName("Alfonso"); 

Cuando esto sucede, el código es mucho más complejo que este ejemplo simplificado. Replicar, encontrar y corregir el error es difícil. Tal vez podría evitarse si pudiésemos marcar ciertas clases como estructuras de datos inmutables y ciertas como si solo tuviéramos objetos inmutables.

La sincronización en una cadena literal o constante definida por un literal de cadena es (potencialmente) un problema ya que el literal de cadena se interna y será compartido por cualquier otra persona en la JVM utilizando el mismo literal de cadena. Sé que este problema ha surgido en los servidores de aplicaciones y otros escenarios de “contenedor”.

Ejemplo:

 private static final String SOMETHING = "foo"; synchronized(SOMETHING) { // } 

En este caso, cualquiera que use la cadena “foo” para bloquear está compartiendo el mismo candado.

Creo que en el futuro el principal problema con Java serán las (falta de) garantías de visibilidad para los constructores. Por ejemplo, si crea la siguiente clase

 class MyClass { public int a = 1; } 

y luego solo lea la propiedad MyClass a desde otro subproceso, MyClass.a podría ser 0 o 1, dependiendo de la implementación y 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 el multihilo durante la fase de inicialización.

El error más tonto que hago con frecuencia es olvidarme de sincronizar antes de llamar a notify () o wait () en un objeto.

Usando un “nuevo Objeto ()” local como mutex.

 synchronized (new Object()) { System.out.println("sdfs"); } 

Esto es inútil.

Otro problema común de ‘concurrencia’ es usar código sincronizado cuando no es necesario en absoluto. Por ejemplo, todavía veo progtwigdores usando StringBuffer o incluso java.util.Vector (como variables locales de método).

Múltiples objetos que están bloqueados pero a los que comúnmente se accede sucesivamente. Nos hemos topado con un par de casos en los que los lockings se obtienen por código diferente en diferentes órdenes, lo que resulta en un punto muerto.

Sin darme cuenta de que this en una clase interna no es el de la clase externa. Normalmente en una clase interna anónima que implementa Runnable . El problema de raíz es que debido a que la sincronización forma parte de todos los Object s, no existe una comprobación de tipo estática. Lo he visto al menos dos veces en Usenet, y también aparece en Brian Goetz’z Java Concurrency in Practice.

Los cierres BGGA no sufren de esto ya que no existe this para el cierre ( this referencia a la clase externa). Si utiliza objetos que no son de este tipo, se soluciona este problema y otros.

Uso de un objeto global como una variable estática para bloquear.

Esto conduce a un rendimiento muy malo debido a la discordia.

Honesly? Antes del advenimiento de java.util.concurrent , el problema más común con el que me tropezaba rutinariamente era lo que llamo “thread-thrashing”: aplicaciones que usan subprocesos para la concurrencia, pero engendran demasiados y terminan azotando.