¿Las variables estáticas se comparten entre hilos?

95

Mi profesor en una clase de Java de nivel superior sobre subprocesos dijo algo de lo que no estaba seguro.

Dijo que el siguiente código no actualizaría necesariamente la readyvariable. Según él, los dos hilos no comparten necesariamente la variable estática, específicamente en el caso en que cada hilo (hilo principal versusReaderThread ) se ejecuta en su propio procesador y, por lo tanto, no comparte los mismos registros / caché / etc. y una CPU. no actualizará el otro.

Esencialmente, dijo que es posible que readyse actualice en el hilo principal, pero NO en el ReaderThread, por lo queReaderThread repetirá infinitamente.

También afirmó que era posible que el programa imprimiera 0o 42. Entiendo cómo 42podría imprimirse, pero no 0. Mencionó que este sería el caso cuando elnumber variable se establece en el valor predeterminado.

Pensé que tal vez no se garantiza que la variable estática se actualice entre los subprocesos, pero esto me parece muy extraño para Java. ¿Hacer readyvolátiles corrige este problema?

Mostró este código:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}
dontocsata
fuente
La visibilidad de las variables no locales no depende de si son variables estáticas, campos de objeto o elementos de matriz, todas tienen las mismas consideraciones. (Con el problema de que los elementos de la matriz no se pueden convertir en volátiles.)
Paŭlo Ebermann
1
pregúntale a tu profesor qué tipo de arquitectura considera que sería posible ver "0". Sin embargo, en teoría, tiene razón.
bestsss
4
@bestsss Hacer ese tipo de pregunta le revelaría al maestro que se había perdido todo el sentido de lo que estaba diciendo. El punto es que los programadores competentes entienden qué está garantizado y qué no y no confían en cosas que no están garantizadas, al menos no sin comprender con precisión qué no está garantizado y por qué.
David Schwartz
Se comparten entre todo lo que carga el mismo cargador de clases. Incluyendo hilos.
Marqués de Lorne
Su maestro (y la respuesta aceptada) tienen 100% de razón, pero mencionaré que rara vez sucede, este es el tipo de problema que se esconderá durante años y solo se mostrará cuando sea más dañino. Incluso las pruebas cortas que intentan exponer el problema tienden a actuar como si todo estuviera bien (probablemente porque no tienen tiempo para que la JVM optimice mucho), por lo que es un tema realmente bueno a tener en cuenta.
Bill K

Respuestas:

75

No hay nada especial en las variables estáticas cuando se trata de visibilidad. Si son accesibles, cualquier hilo puede llegar a ellos, por lo que es más probable que vea problemas de concurrencia porque están más expuestos.

Existe un problema de visibilidad impuesto por el modelo de memoria de la JVM. Aquí hay un artículo que habla sobre el modelo de memoria y cómo las escrituras se vuelven visibles para los hilos . No puede contar con los cambios que un hilo hace que se hagan visibles para otros hilos de manera oportuna (en realidad, la JVM no tiene la obligación de hacer que esos cambios sean visibles para usted, en ningún marco de tiempo), a menos que establezca una relación de suceder antes .

Aquí hay una cita de ese enlace (proporcionada en el comentario de Jed Wesley-Smith):

El capítulo 17 de la Especificación del lenguaje Java define la relación de suceder antes de las operaciones de memoria, como las lecturas y escrituras de variables compartidas. Se garantiza que los resultados de una escritura por un hilo serán visibles para una lectura por otro hilo solo si la operación de escritura ocurre antes de la operación de lectura. Las construcciones sincronizadas y volátiles, así como los métodos Thread.start () y Thread.join (), pueden formar relaciones de sucesos anteriores. En particular:

  • Cada acción en un hilo ocurre antes de cada acción en ese hilo que viene más tarde en el orden del programa.

  • Se produce un desbloqueo (bloqueo sincronizado o salida de método) de un monitor, antes de cada bloqueo posterior (bloque sincronizado o entrada de método) de ese mismo monitor. Y debido a que la relación sucede-antes es transitiva, todas las acciones de un hilo antes del desbloqueo suceden antes de todas las acciones posteriores a cualquier hilo que bloquee ese monitor.

  • Una escritura en un campo volátil ocurre antes de cada lectura posterior de ese mismo campo. Las escrituras y lecturas de campos volátiles tienen efectos de coherencia de memoria similares a los de los monitores de entrada y salida, pero no implican bloqueo de exclusión mutua.

  • Una llamada para comenzar en un hilo ocurre antes de cualquier acción en el hilo iniciado.

  • Todas las acciones en un hilo suceden antes de que cualquier otro hilo regrese exitosamente de una unión en ese hilo.

Nathan Hughes
fuente
3
En la práctica, "de manera oportuna" y "siempre" son sinónimos. Sería muy posible que el código anterior nunca terminara.
ÁRBOL
4
Además, esto demuestra otro anti-patrón. No utilice volátiles para proteger más de una parte del estado compartido. Aquí, número y listo son dos estados, y para actualizarlos / leerlos de manera consistente, necesita una sincronización real.
ÁRBOL
5
La parte de hacerse visible eventualmente está mal. Sin sucede-antes de cualquier relación explícita no hay ninguna garantía de que cualquier escritura será nunca ser visto por otro hilo, como el JIT es bastante en su derecho de imponer la lectura en un registro y luego nunca se verá ninguna actualización. Cualquier carga eventual es suerte y no se debe confiar en ella.
Jed Wesley-Smith
2
"a menos que use la palabra clave volátil o sincronice". debe leer "a menos que haya una relación relevante de suceda antes entre el escritor y el lector" y este enlace: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith
2
@bestsss bien visto. Desafortunadamente, ThreadGroup está roto de muchas maneras.
Jed Wesley-Smith
37

Estaba hablando de visibilidad y no debe tomarse demasiado literalmente.

De hecho, las variables estáticas se comparten entre subprocesos, pero los cambios realizados en un subproceso pueden no ser visibles para otro subproceso de inmediato, lo que hace que parezca que hay dos copias de la variable.

Este artículo presenta una vista que es consistente con cómo presentó la información:

Primero, debe comprender algo sobre el modelo de memoria de Java. Me ha costado un poco a lo largo de los años explicarlo brevemente y bien. A día de hoy, la mejor manera que se me ocurre para describirlo es si lo imaginas de esta manera:

  • Cada hilo en Java tiene lugar en un espacio de memoria separado (esto es claramente falso, así que tengan paciencia conmigo).

  • Debe utilizar mecanismos especiales para garantizar que la comunicación se produzca entre estos hilos, como lo haría en un sistema de paso de mensajes.

  • Las escrituras de memoria que ocurren en un hilo pueden "filtrarse" y ser vistas por otro hilo, pero esto no está garantizado de ninguna manera. Sin una comunicación explícita, no puede garantizar qué escrituras son vistas por otros hilos, o incluso el orden en que se ven.

...

modelo de hilo

Pero, de nuevo, esto es simplemente un modelo mental para pensar en hilos y volátiles, no literalmente cómo funciona la JVM.

Bert F
fuente
12

Básicamente es cierto, pero en realidad el problema es más complejo. La visibilidad de los datos compartidos puede verse afectada no solo por las cachés de la CPU, sino también por la ejecución desordenada de las instrucciones.

Por lo tanto, Java define un modelo de memoria , que establece bajo qué circunstancias los subprocesos pueden ver el estado consistente de los datos compartidos.

En su caso particular, agregar volatilegarantiza visibilidad.

axtavt
fuente
8

Por supuesto, son "compartidos" en el sentido de que ambos se refieren a la misma variable, pero no necesariamente ven las actualizaciones del otro. Esto es cierto para cualquier variable, no solo estática.

Y, en teoría, las escrituras realizadas por otro hilo pueden parecer estar en un orden diferente, a menos que se declaren las variables volatileo que las escrituras estén explícitamente sincronizadas.

biziclop
fuente
4

Dentro de un solo cargador de clases, los campos estáticos siempre se comparten. Para limitar explícitamente los datos a los subprocesos, querrá usar una instalación como ThreadLocal.

Kirk Woll
fuente
2

Cuando inicializa la variable de tipo primitivo estático, java asigna un valor predeterminado para las variables estáticas

public static int i ;

cuando define la variable así, el valor predeterminado de i = 0; es por eso que existe la posibilidad de obtener 0. luego, el hilo principal actualiza el valor de boolean ready a true. ya que ready es una variable estática, el hilo principal y el otro hilo hacen referencia a la misma dirección de memoria, por lo que la variable ready cambia. para que el hilo secundario salga del ciclo while e imprima el valor. cuando se imprime, el valor inicializado del número es 0. si el proceso de subproceso ha pasado mientras el bucle antes del subproceso principal actualiza la variable de número. entonces existe la posibilidad de imprimir 0

NuOne
fuente
-2

@dontocsata puedes volver con tu maestro y educarlo un poco :)

pocas notas del mundo real y sin importar lo que veas o te digan. TENGA EN CUENTA que las palabras a continuación se refieren a este caso particular en el orden exacto que se muestra.

Las siguientes 2 variables residirán en la misma línea de caché en prácticamente cualquier arquitectura conocida.

private static boolean ready;  
private static int number;  

Thread.exit (hilo principal) está garantizado para salir y exit se garantiza que causará una barrera de memoria, debido a la eliminación de subprocesos del grupo de subprocesos (y muchos otros problemas). (es una llamada sincronizada, y no veo una única manera de implementarlo sin la parte de sincronización, ya que ThreadGroup también debe terminar si no quedan hilos de demonio, etc.).

¡El hilo iniciado ReaderThreadmantendrá vivo el proceso ya que no es un demonio! Por lo tanto, readyy numberse eliminarán juntos (o el número anterior si se produce un cambio de contexto) y no hay una razón real para reordenar en este caso, al menos ni siquiera puedo pensar en uno. Necesitarás algo realmente extraño para ver cualquier cosa menos 42. Nuevamente, supongo que ambas variables estáticas estarán en la misma línea de caché. Simplemente no puedo imaginar una línea de caché de 4 bytes de largo O una JVM que no los asigne en un área continua (línea de caché).

bestsss
fuente
3
@bestsss, si bien eso es cierto hoy en día, se basa en la implementación actual de JVM y la arquitectura de hardware para su veracidad, no en la semántica del programa. Esto significa que el programa todavía está roto, aunque puede funcionar. Es fácil encontrar una variante trivial de este ejemplo que realmente falla en las formas especificadas.
Jed Wesley-Smith
1
Dije que no sigue las especificaciones, sin embargo, como profesor, al menos encuentra un ejemplo adecuado que realmente pueda fallar en alguna arquitectura de productos básicos, por lo que el ejemplo es algo real.
bestsss
6
Posiblemente el peor consejo que he visto sobre cómo escribir código seguro para subprocesos.
Lawrence Dol
4
@Bestsss: La respuesta simple a su pregunta es simplemente esta: "Codifique según la especificación y el documento, no para los efectos secundarios de su sistema o implementación en particular". Esto es especialmente importante en una plataforma de máquina virtual diseñada para ser independiente del hardware subyacente.
Lawrence Dol
1
@Bestsss: El punto de los profesores es que (a) el código podría funcionar bien cuando lo pruebes, y (b) el código está roto porque depende del funcionamiento del hardware y no de las garantías de la especificación. El punto es que parece que está bien, pero no está bien.
Lawrence Dol