11

Veo este código con bastante frecuencia en algunas pruebas de unidad OSS, pero ¿es seguro para subprocesos? ¿El ciclo while garantiza ver el valor correcto de invoc?¿Hay lectura no sincronizada de enhebrado de enteros en java?

Si no; nerd señala a quien también sabe en qué arquitectura de CPU puede fallar.

private int invoc = 0; 

    private synchronized void increment() { 
    invoc++; 
    } 

    public void isItThreadSafe() throws InterruptedException { 
     for (int i = 0; i < TOTAL_THREADS; i++) { 
     new Thread(new Runnable() { 
      public void run() { 
      // do some stuff 
      increment(); 
      } 
     }).start(); 
     } 
     while (invoc != TOTAL_THREADS) { 
     Thread.sleep(250); 
     } 
    } 
+0

No puedo decir 100%, pero parece que la lectura no sincronizada del campo no volátil no está garantizada para leer el valor más reciente de 'invoc' (a pesar de que las escrituras * * se vacían debido a la palabra clave 'synchronized' en el método' increment() '). Sin embargo, no conoce el modelo de memoria Java lo suficiente como para decirlo con seguridad. – dlev

+0

@dlev Esa es, más o menos, la pregunta;) – krosenvold

+0

Estaba a punto de responder: "¡Siempre me ha funcionado!". Pero también se encontró http://stackoverflow.com/questions/1006655/are-java-primitive-ints-atomic-by-design-or-by-accident que dicen "Sí" para las entradas, y "Quizás" para las que son largas y dobles. . –

Respuesta

17

No, no es seguro para la rosca. invoc debe declararse volátil, o accederse mientras se sincroniza en el mismo bloqueo, o cambiarse para usar AtomicInteger. Simplemente usar el método sincronizado para incrementar invoc, pero no sincronizar para leerlo, no es lo suficientemente bueno.

La JVM realiza muchas optimizaciones, incluido el almacenamiento en caché específico de la CPU y el reordenamiento de instrucciones. Utiliza la palabra clave volátil y el bloqueo para decidir cuándo puede optimizar libremente y cuándo debe tener un valor actualizado disponible para que otros hilos lo lean. Entonces, cuando el lector no usa el bloqueo, la JVM no puede saber que no le dará un valor obsoleto.

Esta cita de Java concurrencia en la práctica (sección 3.1.3) analiza cómo tanto escribe y lee necesitan sincronizarse:

bloqueo intrínseco se puede utilizar para garantizar que un hilo ve los efectos de otro de manera predecible, como se ilustra en la Figura 3.1. Cuando el hilo A ejecuta un bloque sincronizado y posteriormente el hilo B entra en un bloque sincronizado protegido por el mismo bloqueo, se garantiza que los valores de las variables que fueron visibles para A antes de soltar el bloqueo son visibles para B al adquirir el bloqueo. En otras palabras, todo lo que hacía A en o antes de un bloque sincronizado es visible para B cuando ejecuta un bloque sincronizado protegido por el mismo bloqueo. Sin sincronización, no hay tal garantía.

La siguiente sección (3.1.4) cubre el uso volátil:

El lenguaje Java proporciona también una alternativa, forma más débil de la sincronización, las variables volátiles, para garantizar que las actualizaciones de una variable se propagan predeciblemente a otros hilos. Cuando un campo se declara volátil, el compilador y el tiempo de ejecución reciben aviso de que esta variable se comparte y que las operaciones que se realizan en ella no se deben reordenar con otras operaciones de memoria. Las variables volátiles no se almacenan en caché en registros o en cachés donde están ocultos de otros procesadores, por lo que una lectura de una variable volátil siempre devuelve la escritura más reciente de cualquier subproceso.

Cuando todos teníamos máquinas con una sola CPU en nuestros equipos de sobremesa, escribíamos código y nunca teníamos problemas hasta que se ejecutaba en un cuadro de multiprocesador, generalmente en producción. Algunos de los factores que dan lugar a los problemas de visibilidad, como las cachés locales de la CPU y el reordenamiento de instrucciones, son cosas que cabría esperar de cualquier máquina multiprocesador. Sin embargo, la eliminación de instrucciones aparentemente innecesarias podría ocurrir para cualquier máquina. No hay nada que obligue a la JVM a hacer que el lector vea el valor actualizado de la variable, usted está a merced de los implementadores de JVM. Entonces me parece que este código no sería una buena apuesta para ninguna arquitectura de CPU.

2

¡Bueno!

private volatile int invoc = 0; 

Harán el truco.

Y vea Are java primitive ints atomic by design or by accident? que ubica algunas de las definiciones java relevantes. Aparentemente int está bien, pero puede que no sea el doble & long.


editar, complemento. La pregunta pregunta, "¿ves el valor correcto de invoc?". ¿Cuál es "el valor correcto"? Como en el continuo del espacio de tiempo, la simultaneidad no existe realmente entre los hilos. Una de las publicaciones anteriores señala que el valor eventualmente se enrojecerá, y el otro subproceso lo obtendrá. ¿El código es "thread safe"? Diría que "sí", porque no se "portará mal" en función de los caprichos de la secuencia, en este caso.

+1

No estoy seguro de que alguna de las citas en la pregunta mencionada en realidad cubra la visibilidad de un hilo diferente – krosenvold

+1

Sin ser metafísico, creo que hay una posibilidad teórica de el bucle externo nunca verá nada excepto 0. – krosenvold

0

Por lo que yo entiendo el código debe ser seguro. El bytecode se puede reordenar, sí. Pero eventualmente invoc debería estar sincronizado nuevamente con el hilo principal. Sincronice las garantías de que la invocación se incrementa correctamente, de modo que haya una representación consistente de la invocación en algún registro. En algún momento, este valor se borrará y la pequeña prueba tendrá éxito.

Ciertamente, no es bueno y yo iría con la respuesta que voté y corregirá este código porque huele. Pero al pensarlo, lo consideraría seguro.

+0

Se trata más de visibilidad, menos de reordenamiento. La VM ** puede ** optimizar la lectura de la memoria por completo y tratar _invoc_ como una constante en la secuencia donde el acceso no está sincronizado/volátil. Así que esto tiene que ser resuelto, aunque podría funcionar a veces o incluso a menudo. – Boris

+0

Sé que debería confiar en mi instinto sintiéndome más :-) –

1

Teóricamente, es posible que la lectura esté en caché. Nada en el modelo de memoria de Java impide eso.

Prácticamente, esto es muy poco probable que suceda (en su caso particular). La pregunta es si JVM puede optimizar en una llamada a método.

read #1 
method(); 
read #2 

Para JVM a la razón de que leer # 2 se puede volver a utilizar el resultado de la lectura # 1 (que puede ser almacenado en un registro de la CPU), debe estar seguro de que method() no contiene acciones de sincronización. Esto es generalmente imposible, a menos que method() esté en línea, y JVM puede ver desde el código sin formato que no hay sincronización/volátil u otras acciones de sincronización entre la lectura # 1 y la lectura # 2; entonces puede eliminar con seguridad la lectura n. ° 2.

En su ejemplo, el método es Thread.sleep(). Una forma de implementarlo es con el bucle ocupado durante ciertos tiempos, dependiendo de la frecuencia de la CPU. Entonces JVM puede alinearlo, y luego eliminar la lectura # 2.

Pero, por supuesto, tales implementación de sleep() es poco realista. Por lo general, se implementa como un método nativo que llama kernel OS. La pregunta es si JVM puede optimizar a través de un método nativo.

Incluso si JVM tiene conocimiento del funcionamiento interno de algunos métodos nativos, por lo tanto puede optimizarlos, es improbable que sleep() se trate de esa manera. sleep(1ms) requiere millones de ciclos de CPU para regresar, realmente no tiene sentido optimizarlo para guardar algunas lecturas.

-

Esta discusión revela que el mayor problema de las razas de datos - se necesita mucho esfuerzo para razonar sobre ello. Un programa no es necesariamente incorrecto, si no está "correctamente sincronizado", sin embargo, demostrar que no está mal no es una tarea fácil. La vida es mucho más simple, si un programa está sincronizado correctamente y no contiene datos de raza.

+0

Lo raro es que estoy casi seguro de que vi este caso exacto sucediendo, teórica o no. – krosenvold

0

Si usted no está obligado a usar "int", sugeriría AtomicInteger como una alternativa segura para los subprocesos.