¿Cuál es el problema de concurrencia más frecuente que ha encontrado en Java? [cerrado]


191

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.


16
¿Por qué está cerrado esto? Esto es útil tanto para otros programadores que piden simultaneidad en Java, como para tener una idea de qué clases de defectos de simultaneidad están siendo observados más por otros desarrolladores de Java.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

@Longpoke El mensaje de cierre explica por qué está cerrado. Esta no es una pregunta con una respuesta "correcta" específica, es más una pregunta de encuesta / lista. Y Stack Overflow no tiene la intención de alojar este tipo de preguntas. Si no está de acuerdo con esa política, es posible que desee discutirla en meta .
Andrzej Doyle

77
¡Supongo que la comunidad no está de acuerdo ya que este artículo está obteniendo más de 100 visitas / día! Lo he encontrado muy útil ya que estoy involucrado en el desarrollo de una herramienta de análisis estático específicamente diseñada para solucionar problemas de concurrencia contemplateltd.com/threadsafe . Tener un banco de problemas de concurrencia comúnmente encontrados ha sido excelente para probar y mejorar ThreadSafe.
Craig Manson

La lista de verificación de revisión de código para Java Concurrency resuelve la mayoría de las trampas mencionadas en las respuestas a esta pregunta en una forma conveniente para las revisiones de código del día a día.
leventov

Respuestas:


125

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:

class MyThread extends Thread {
  private boolean stop = false;

  public void run() {
    while(!stop) {
      doSomeWork();
    }
  }

  public void setStop() {
    this.stop = true;
  }
}

Mientras parada no es volátil o setStopy runno 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.


9
Una gran solución para esto es hacer que la instancia de detención sea un AtomicBoolean. Resuelve todos los problemas de los no volátiles, mientras te protege de los problemas de JMM.
Kirk Wylie el

39
Es peor que 'por varios minutos', es posible que NUNCA lo veas. Bajo el modelo de memoria, la JVM puede optimizar while (! Stop) en while (verdadero) y luego se le aplica una manguera. Esto solo puede suceder en algunas máquinas virtuales, solo en modo servidor, solo cuando la JVM se vuelve a compilar después de x iteraciones del bucle, etc. ¡Ay!
Cowan

2
¿Por qué querrías usar AtomicBoolean sobre boolean volátil? Estoy desarrollando para la versión 1.4+, entonces, ¿hay algún inconveniente con solo declarar volátil?
Pool

2
Nick, creo que es porque el CAS atómico suele ser incluso más rápido que volátil. Si está desarrollando para su 1.4 mi humilde opinión opción sólo es seguro es utilizar sincronizada tan volátil en 1.4 no tiene los fuertes garantías de barrera de memoria, ya que tiene en Java 5.
kutzi

55
@Thomas: eso se debe al modelo de memoria Java. Debería leer sobre ello si desea conocerlo en detalle (Java Concurrency in Practice de Brian Goetz lo explica bien, por ejemplo). En resumen: a menos que use palabras clave / construcciones de sincronización de memoria (como volátil, sincronizado, AtomicXyz, pero también cuando un subproceso está terminado) un subproceso NO tiene garantía alguna para ver los cambios realizados en cualquier campo realizado por un subproceso diferente
Kutzi

178

Mi problema de concurrencia más doloroso número 1 ocurrió 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; Debido a que 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 diferentes entre sí). El resultado es obviamente malo.


62
Esta es una de las razones por las que prefiero el bloqueo de objeto final estático privado = new Object ();
Andrzej Doyle

17
Me encanta - oh, esto es desagradable :)
Thorbjørn Ravn Andersen

77
Eso es bueno para Java Puzzlers 2.
Dov Wasserman

12
En realidad ... realmente me hace querer que el compilador se niegue a permitirte sincronizar en una cadena. Dado el internamiento de String, no hay ningún caso en el que eso sea "algo bueno (tm)".
Jared

3
@Jared: "hasta que la cadena esté internada" no tiene sentido. Las cuerdas no "se convierten" mágicamente en un interno. String.intern () devuelve un objeto diferente, a menos que ya tenga la instancia canónica de la cadena especificada. Además, todas las cadenas literales y expresiones constantes con valores de cadena están internados. Siempre. Vea los documentos para String.intern () y §3.10.5 de JLS.
Laurence Gonsalves

65

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

synchronized(foo) {
  foo = ...
}

Otros hilos concurrentes se sincronizan en un objeto diferente y este bloque no proporciona la exclusión mutua que espera.


19
Hay una inspección de IDEA para esto llamada "Sincronización en campo no final que probablemente no tenga semántica útil". Muy agradable.
Jen S.

8
Ja ... ahora esa es una descripción torturada. "poco probable que tenga una semántica útil" podría describirse mejor como "muy probablemente roto" :)
Alex Miller

Creo que fue Bitter Java el que tuvo esto en su ReadWriteLock. Afortunadamente ahora tenemos java.util.concurrency.locks, y Doug está un poco más en la pelota.
Tom Hawtin - tackline el

También he visto este problema a menudo. Solo sincronizar en objetos finales, para el caso. FindBugs y col. ayuda, si.
gimpf

¿Es esto solo un problema durante la asignación? (vea el ejemplo de @Alex Miller a continuación con un mapa) ¿Ese ejemplo de mapa también tendría el mismo problema?
Alex Beardsley

50

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.


¿Conoces algún proyecto de código abierto que contenga este error en alguna versión? Estoy buscando ejemplos concretos de este error en el software del mundo real.
reprogramador

47

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:

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 hilo 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, 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 .


77
El patrón enton singleton resuelve todos estos problemas (ver los comentarios de Josh Bloch sobre esto). El conocimiento de su existencia debería estar más extendido entre los programadores de Java.
Robin

Todavía tengo que encontrar un caso en el que la inicialización diferida de un singleton sea realmente apropiada. Y si es así, simplemente declare el método sincronizado.
Dov Wasserman

3
Esto es lo que uso para la inicialización diferida de las clases Singleton. Tampoco se requiere sincronización ya que esto está garantizado por Java implícitamente. clase Foo {titular de clase estática {static Foo foo = new Foo (); } static Foo getInstance () {return Holder.foo; }}
Irfan Zulfiqar

Irfan, eso se llama el método de Pugh, por lo que recuerdo
Chris R

@Robin, ¿no es más simple usar un inicializador estático? Esos siempre están garantizados para ejecutarse sincronizados.
matt b

47

No se sincroniza correctamente en los objetos devueltos por Collections.synchronizedXXX(), especialmente durante la iteración o las operaciones múltiples:

Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

...

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

Eso esta mal . A pesar de las operaciones individuales synchronized, el estado del mapa entre la invocación containsy putpuede ser cambiado por otro hilo. Debería ser:

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

O con una ConcurrentMapimplementación:

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

55
O mejor, use un ConcurrentHashMap y putIfAbsent.
Tom Hawtin - tackline

37

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:

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

No, eso es totalmente lo que estoy buscando. ¡Gracias!
Alex Miller

30

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:

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

Por ejemplo, en la segunda línea anterior, los métodos toArray()y size()son seguros para subprocesos por derecho propio, pero size()se evalúa por separado de toArray(), 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.


55
buen ejemplo. Creo que lo atribuiría de manera más general ya que "la composición de las operaciones atómicas no es atómica". (Ver campo volátil ++ para otro ejemplo simple)
Alex Miller

29

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.


una de esas respuestas me gustaría poder dar más de un punto por
Epaga

2
EDT = Hilo de envío de eventos
mjj1409

28

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:

 synchronized (obj) {
     while (<condition does not hold>) {
         obj.wait();
     }
     // do stuff based on condition being true
 }

26

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.


Sí, y hay buenas herramientas para manejar esto ahora con controladores.
Alex Miller

3
¿Podría publicar enlaces a algún artículo o referencia que explique esto con mayor detalle?
Abhijeet Kashnia

22

Hasta que tomó una clase con Brian Goetz que no se dio cuenta de que la no sincronizada getterde un campo privado mutado a través de un sincronizado setterse 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.

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;  
    }
}

55
En las JVM posteriores (1.5 y posteriores, creo), el uso de volátiles también solucionará eso.
James Schek el

2
No necesariamente. volatile le ofrece el último valor, por lo que evita la devolución de 1 para siempre, pero no proporciona bloqueo. Está cerca, pero no es lo mismo.
John Russell

1
@JohnRussell Pensé que volátil garantiza una relación de suceder antes. no es eso "bloqueo"? "Una escritura en una variable volátil (§8.3.1.4) v se sincroniza con todas las lecturas subsiguientes de v por cualquier hilo (donde subsecuente se define de acuerdo con el orden de sincronización)".
Shawn

15

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.


3
Si, de hecho! La estática mutable rompe el confinamiento del hilo. Sorprendentemente, nunca encontré nada sobre este escollo ni en JCiP ni en CPJ.
Julien Chastang el

Espero que esto sea obvio para la gente que hace programación concurrente. El estado global debería ser el primer lugar para verificar la seguridad de los hilos.
gtrak

1
@Gary Thing es que no piensan que están haciendo programación concurrente.
Tom Hawtin - entrada el

15

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.


13

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.


11

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.


11

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.

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 ...
    }
}

10

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.


9

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.


¿Puedes dividir el último en una respuesta separada? Mantengamos 1 por publicación. Estos son dos muy buenos.
Alex Miller

9

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 .


8

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. Quizás podría evitarse si pudiéramos marcar ciertas clases como inmutables y ciertas estructuras de datos como que solo contienen objetos inmutables.


8

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:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

En este caso, cualquiera que use la cadena "foo" para bloquear está compartiendo el mismo bloqueo.


Potencialmente está bloqueado. El problema es que la semántica en CUANDO las cadenas están internados es indefinida (o, IMNSHO, subdefinida). Se internó una constante de tiempo de compilación de "foo", "foo" que viene de una interfaz de red solo se internó si lo hace así.
Kirk Wylie el

Correcto, es por eso que específicamente utilicé una constante de cadena literal, que se garantiza que será internada.
Alex Miller

8

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

class MyClass {
    public int a = 1;
}

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.


Esto me parece un poco sorprendente, pero sé que eres un tipo inteligente, Tim, así que lo tomaré sin referencia. :) Sin embargo, si un fuera definitivo, esto no sería una preocupación, ¿correcto? ¿Entonces estaría sujeto a la semántica de congelación final durante la construcción?
Alex Miller

Todavía encuentro cosas en el JMM que me sorprenden, así que no confiaría en mí, pero estoy bastante seguro de esto. Ver también cs.umd.edu/~pugh/java/memoryModel/… . Si el campo fuera definitivo, no sería un problema, entonces sería visible después de la fase de inicialización.
Tim Jansen el

2
Esto es solo un problema, si la referencia de la instancia recién creada ya está en uso antes de que el constructor haya regresado / terminado. Por ejemplo, la clase se registra durante la construcción en un grupo público y otros subprocesos comienzan a acceder a ella.
ReneS

3
MyClass.a indica acceso estático y 'a' no es un miembro estático de MyClass. Aparte de eso, es como dice 'ReneS', esto es solo un problema si se filtra una referencia al objeto incompleto, como agregar 'esto' a algún mapa externo en el constructor, por ejemplo.
Markus Jevring

7

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


8
A diferencia de la mayoría de los problemas de concurrencia, ¿no es fácil de encontrar? Al menos obtienes una IllegalMonitorStateException aquí ...
Outlaw Programmer

Afortunadamente, es muy fácil de encontrar ... pero aún así es un error tonto que los residuos mi tiempo más de lo que debería :)
David Ray

7

Usando un "nuevo Object ()" local como mutex.

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

Esto es inútil


2
Probablemente esto sea inútil, pero el acto de sincronizar hace algunas cosas interesantes ... Ciertamente, crear un nuevo Objeto cada vez es un desperdicio completo.
ÁRBOL

44
No es inútil Es una barrera de memoria sin cerradura.
David Roussel

1
@David: el único problema: jvm podría optimizarlo eliminando ese bloqueo en absoluto
yetanothercoder

@insighter Veo que tu opinión es compartida ibm.com/developerworks/java/library/j-jtp10185/index.html Estoy de acuerdo en que es algo tonto, ya que no sabes cuándo se sincronizará tu barrera de memoria, yo solo señalaba que estaba haciendo más que nada.
David Roussel

7

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 StringBuffero incluso java.util.Vector(como variables locales del método).


1
Esto no es un problema, pero es innecesario, ya que le dice a la JVM que sincronice los datos con la memoria global y, por lo tanto, podría funcionar mal en múltiples cpus, aun así, nadie usa el bloque de sincronización de manera concurrente.
ReneS

6

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.


5

No darse cuenta de que el thisde una clase interna no es el thisde la clase externa. Típicamente en una clase interna anónima que implementa Runnable. 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 thispara el cierre (hace thisreferencia a la clase externa). Si usa no thisobjetos como bloqueos, entonces se soluciona este problema y otros.


3

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.


Bueno, a veces, a veces no. Si sólo sería tan fácil ...
gimpf

Suponiendo que el subproceso ayuda a aumentar el rendimiento del problema dado, siempre degrada el rendimiento tan pronto como más de un subproceso accede al código que está protegido por el bloqueo.
kohlerm

3

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.


¿Estás insinuando que te encuentras con más problemas ahora que java.util.concurrent está disponible?
Andrzej Doyle el
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.