Las variables en Java se dividen en dos categorías: variables locales y variables de clase. Las variables locales se refieren a variables definidas dentro de un método, como las variables definidas en el método de ejecución. Para estas variables, no hay problema de compartir entre subprocesos. Por tanto, no requieren sincronización de datos. Las variables de clase son variables definidas en una clase y su alcance es toda la clase. Estas variables pueden ser compartidas por varios subprocesos. Por lo tanto, necesitamos realizar una sincronización de datos en dichas variables.
La sincronización de datos significa que solo un subproceso puede acceder a las variables de clase sincronizadas al mismo tiempo. Una vez que el subproceso actual ha accedido a estas variables, otros subprocesos pueden continuar accediendo a ellas. El acceso mencionado aquí se refiere al acceso con operaciones de escritura. Si todos los subprocesos que acceden a variables de clase son operaciones de lectura, generalmente no se requiere sincronización de datos. Entonces, ¿qué sucede si no se realiza la sincronización de datos en variables de clase compartida? Primero veamos qué sucede con el siguiente código:
Copie el código de código de la siguiente manera:
prueba de paquete;
clase pública MyThread extiende Thread
{
público estático int n = 0;
ejecución pública vacía()
{
int m = n;
producir();
m++;
norte = metro;
}
public static void main (String[] args) lanza una excepción
{
MiHilo miHilo = nuevo MiHilo ();
Hilos de hilo[] = nuevo hilo[100];
para (int i = 0; i < threads.length; i++)
hilos[i] = nuevo hilo(mihilo);
para (int i = 0; i < threads.length; i++)
hilos[i].start();
para (int i = 0; i < threads.length; i++)
hilos[i].join();
System.out.println("n = " + MyThread.n);
}
}
Los posibles resultados de ejecutar el código anterior son los siguientes:
Copie el código de código de la siguiente manera:
n=59
Muchos lectores se sorprenderán al ver este resultado. Este programa obviamente inicia 100 subprocesos y luego cada subproceso incrementa la variable estática n en 1. Finalmente, use el método de unión para ejecutar los 100 subprocesos y luego genere el valor n. Normalmente, el resultado debería ser n = 100. Pero el resultado es menos de 100.
De hecho, el culpable de este resultado son los "datos sucios" que mencionamos a menudo. La declaración de rendimiento () en el método de ejecución es el iniciador de "datos sucios" (sin agregar una declaración de rendimiento, también se pueden generar "datos sucios", pero no será tan obvio. Solo cambiando 100 a un número mayor Esto ocurre a menudo. Generar "datos sucios", llamar a rendimiento en este ejemplo es amplificar el efecto de los "datos sucios"). La función del método de rendimiento es pausar el subproceso, es decir, hacer que el subproceso que llama al método de rendimiento ceda temporalmente recursos de la CPU, dándole a la CPU la oportunidad de ejecutar otros subprocesos. Para ilustrar cómo este programa genera "datos sucios", supongamos que sólo se crean dos subprocesos: subproceso1 y subproceso2. Dado que el método de inicio de thread1 se llama primero, el método de ejecución de thread1 generalmente se ejecutará primero. Cuando el método de ejecución de thread1 se ejecuta hasta la primera línea (int m = n;), el valor de n se asigna a m. Cuando se ejecuta el método de rendimiento de la segunda línea, el subproceso1 dejará de ejecutarse temporalmente. Cuando el subproceso1 está en pausa, el subproceso2 comienza a ejecutarse después de obtener los recursos de la CPU (el subproceso2 ha estado listo antes cuando el subproceso2 ejecuta la primera línea). m = n;), dado que n sigue siendo 0 cuando se ejecuta el hilo1 para producir rendimiento, el valor obtenido por m en el hilo2 también es 0. Esto hace que los valores m de thread1 y thread2 obtengan 0. Después de ejecutar el método de rendimiento, todos comienzan desde 0 y suman 1. Por lo tanto, no importa quién lo ejecute primero, el valor final de n es 1, pero a este n se le asigna un valor por thread1 y thread2 respectivamente. Alguien puede preguntar, si solo hay n++, ¿se generarán "datos sucios"? La respuesta es sí. Entonces n++ es solo una declaración, entonces, ¿cómo entregar la CPU a otros subprocesos durante la ejecución? De hecho, esto es solo un fenómeno superficial. Después de que el compilador de Java compila n ++ en un lenguaje intermedio (también llamado código de bytes), ya no es un lenguaje. Veamos en qué lenguaje intermedio Java se compilará el siguiente código Java.
Copie el código de código de la siguiente manera:
ejecución pública vacía()
{
n++;
}
Código de lenguaje intermedio compilado
Copie el código de código de la siguiente manera:
ejecución pública vacía()
{
carga_0
duplicar
getfield
iconost_1
iadd
putfield
devolver
}
Puede ver que solo hay declaraciones n ++ en el método de ejecución, pero después de la compilación, hay 7 declaraciones de lenguaje intermedio. No necesitamos saber cuáles son las funciones de estas declaraciones, solo mire las declaraciones en las líneas 005, 007 y 008. La línea 005 es getfield. Según su significado en inglés, sabemos que queremos obtener un determinado valor. Debido a que aquí solo hay un n, no hay duda de que queremos obtener el valor de n. No es difícil adivinar que el iadd en la línea 007 es sumar 1 al valor n obtenido. Creo que habrás adivinado el significado de putfield en la línea 008. Es responsable de actualizar n después de agregar 1 nuevamente a la variable de clase n. Hablando de esto, es posible que todavía tengas dudas. Al ejecutar n++, basta con sumar n por 1. ¿Por qué es tan complicado? De hecho, esto implica un problema con el modelo de memoria de Java.
El modelo de memoria de Java se divide en área de almacenamiento principal y área de almacenamiento de trabajo. El área de almacenamiento principal almacena todas las instancias en Java. Es decir, después de usar new para crear un objeto, el objeto y sus métodos internos, variables, etc. se guardan en esta área, y n en la clase MyThread se guarda en esta área. El almacenamiento principal puede ser compartido por todos los subprocesos. El área de almacenamiento de trabajo es la pila de subprocesos de la que hablamos anteriormente. En esta área, se almacenan las variables definidas en el método de ejecución y el método llamado por el método de ejecución, es decir, las variables del método. Cuando un hilo quiere modificar las variables en el área de almacenamiento principal, no modifica estas variables directamente, sino que las copia en el área de almacenamiento de trabajo del hilo actual. Después de la modificación, el valor de la variable se sobrescribe con el valor correspondiente. en el área de almacenamiento principal valor variable.
Después de comprender el modelo de memoria de Java, no es difícil entender por qué n++ no es una operación atómica. Debe pasar por un proceso de copiar, agregar 1 y sobrescribir. Este proceso es similar al simulado en la clase MyThread. Como puede imaginar, si thread1 se interrumpe por algún motivo cuando se ejecuta getfield, ocurrirá una situación similar al resultado de la ejecución de la clase MyThread. Para resolver completamente este problema, debemos usar algún método para sincronizar n, es decir, solo un hilo puede operar n al mismo tiempo, lo que también se llama operación atómica en n.