Primero leemos una explicación detallada de sincronizado:
Sincronized es una palabra clave en el lenguaje Java. Cuando se usa para modificar un método o un bloque de código, puede asegurarse de que, como máximo, un hilo ejecuta el código al mismo tiempo.
1. Cuando dos hilos concurrentes acceden a este bloque de código sincronizado (este) sincronizado en el mismo objeto de objeto, solo se puede ejecutar un hilo dentro de una vez. Otro hilo debe esperar a que el hilo actual ejecute este bloque de código antes de que pueda ejecutar el bloque de código.
2. Sin embargo, cuando un hilo accede a un bloque de código de sincronización sincronizado (este) de un objeto, otro hilo aún puede acceder al bloque de código de sincronización no sincronizado (esto) en ese objeto.
3. Es particularmente crítico que cuando un subproceso acceda a un bloque de código de sincronización sincronizado (este) de un objeto, se bloquearán otros hilos para acceder a todos los otros bloques de código de sincronización sincronizados (esto) en el objeto.
4. El tercer ejemplo también se aplica a otros bloques de código sincrónicos. Es decir, cuando un hilo accede a un bloque de código de sincronización sincronizado (este) de un objeto, obtiene el bloqueo del objeto de este objeto. Como resultado, otros subprocesos acceden a todas las partes de código sincrónico del objeto del objeto se bloquea temporalmente.
5. Las reglas anteriores también se aplican a otros bloqueos de objetos.
En pocas palabras, sincronizado declara un bloqueo para el hilo actual. El hilo con este bloqueo puede ejecutar instrucciones en el bloque, y otros hilos solo pueden esperar a que el bloqueo lo adquiera antes de la misma operación.
Esto es muy útil, pero encontré otra situación extraña.
1. En la misma clase, hay dos métodos: el uso de la declaración de palabras clave sincronizada
2. Al ejecutar uno de los métodos, debe esperar a que se ejecute el otro método (devolución de llamada de hilo asíncrono), para que use un CountdownLatch para esperar.
3. El código se deconstruye de la siguiente manera:
sincronizado void a () {CountDownLatch = new CountdownLatch (1); // Do Someing CountdownLatch.AWAIT ();} sincronizado vacío b () {CountdownLatch.countDown ();} en
El método A es ejecutado por el hilo principal, el método B se ejecuta mediante el hilo asíncrono y el resultado de la ejecución de la devolución de llamada es:
El hilo principal comienza a atascarse después de ejecutar el método A, y ya no lo hace, y será inútil que espere sin importar cuánto tiempo lleva.
Este es un problema clásico de punto muerto
A espera a que B se ejecute, pero de hecho, no piense que B es una devolución de llamada, B también está esperando que A ejecute. ¿Por qué? Sincronizado juega un papel.
En términos generales, cuando queremos sincronizar un bloque de código, necesitamos usar una variable compartida para bloquearlo, por ejemplo:
byte [] mutex = new byte [0]; void a1 () {sincronizado (mutex) {// dosomething}} void b1 () {sincronizado (mutex) {// dosiM Si el contenido del método A y el método B se migran a los bloques sincronizados de los métodos A1 y B1, respectivamente, será fácil de entender.
Después de ejecutar A1, esperará indirectamente que se ejecute el método (CountdownLatch) B1.
Sin embargo, dado que el Mutex en A1 no se lanza, comenzamos a esperar B1. En este momento, incluso si el método B1 de devolución de llamada asíncrono debe esperar a que Mutex libere el bloqueo, el método B no se ejecutará.
¡Esto causó un punto muerto!
La palabra clave sincronizada aquí se coloca frente al método, y la función es la misma. Es solo que el lenguaje Java te ayuda a ocultar la declaración y el uso de Mutex. El método sincronizado utilizado en el mismo objeto es el mismo, por lo que incluso una devolución de llamada asíncrona causará puntos de punto, así que preste atención a este problema. Este nivel de error es que la palabra clave sincronizada se usa incorrectamente. No lo use al azar y úselo correctamente.
Entonces, ¿cuál es exactamente un objeto mutex invisible?
El ejemplo en sí es fácil de pensar. Porque de esta manera, no hay necesidad de definir un nuevo objeto y hacer un bloqueo. Para probar esta idea, puede escribir un programa para probarlo.
La idea es muy simple. Defina una clase y hay dos métodos. Uno se declara sincronizado y el otro se usa sincronizado (esto) en el cuerpo del método. Luego comience dos hilos para llamar a estos dos métodos por separado. Si se produce una competencia de bloqueo entre los dos métodos (espera), se puede explicar que el mutex invisible sincronizado declarado por el método es en realidad la instancia en sí.
public class múltiple Multithreadsync {public sincronizado void m1 () lanza interruptedException {sistema. out.println ("m1 llamada"); Hilo. Sleep (2000); Sistema. out.println ("m1 llamado hecho"); } public void m2 () lanza interruptedException {sincronizado (this) {sistema. out.println ("m2 llamado"); Hilo. Sleep (2000); Sistema. out.println ("m2 llamado hecho"); }} public static void main (string [] args) {final multithreadsync thisObj = new múltiple Multithreadsync (); Thread t1 = new Thread () {@Override public void run () {try {thisObj.m1 (); } catch (InterruptedException e) {E.PrintStackTrace (); }}}}; Thread t2 = new Thread () {@Override public void run () {try {thisObj.m2 (); } catch (InterruptedException e) {E.PrintStackTrace (); }}}; t1.start (); t2.start (); }} La salida del resultado es:
M1 Callm1 Llame a Donem2 Callm2 Llamada realizada
Se explica que el bloque de sincronización del método M2 está esperando la ejecución de M1. Esto puede confirmar el concepto anterior.
Cabe señalar que cuando la sincronización se agrega al método estático, ya que es un método a nivel de clase, el objeto bloqueado es la instancia de clase de la clase actual. También puede escribir un programa para demostrarlo. Aquí se omite.
Por lo tanto, la palabra clave sincronizada del método se puede reemplazar automáticamente con sincronizado (esto) {} al leerla, lo cual es fácil de entender.
Void Method () {void sincronizado método () {sincronizado (this) {// código biz // código biz} ------ >>>}} Visibilidad de la memoria de sincronizado
En Java, todos sabemos que la palabra clave sincronizada se puede usar para implementar la exclusión mutua entre hilos, pero a menudo olvidamos que tiene otra función, es decir, para garantizar la visibilidad de las variables en la memoria, es decir, cuando dos subprocesos leen y escriben acceso a la misma variable al mismo tiempo, sincronizado se usa sincronizado de nuevo.
Por ejemplo, el siguiente ejemplo:
clase pública Novisibility {private static boolean Ready = false; Número de intst intivit intivate = 0; Private static class ReadThread extiende el hilo {@Override public void run () {while (! Ready) {thread.yield (); // admite la CPU para permitir que otros hilos funcionen} System.out.println (número); }} public static void main (string [] args) {new ReaderThread (). Start (); número = 42; Ready = True; }}¿Qué crees que emitirá leer los subprocesos? 42? En circunstancias normales, 42 se emitirán. Sin embargo, debido a los problemas de reordenamiento, el subproceso de lectura puede generar 0 o no emitir nada.
Sabemos que el compilador puede reordenar el código al compilar el código Java en ByTecode, y la CPU también puede reordenar sus instrucciones al ejecutar las instrucciones de la máquina. Mientras el reordenamiento no destruya la semántica del programa
En un solo hilo, siempre que el reordenamiento no afecte el resultado de la ejecución del programa, no se puede garantizar que las operaciones en él deben ejecutarse en el orden especificado por el programa, incluso si el reordenamiento puede tener un impacto significativo en otros hilos.
Esto significa que la ejecución de la declaración "Ready = True" puede tener prioridad sobre la ejecución de la declaración "Número = 42". En este caso, el subproceso de lectura puede generar el valor predeterminado del número 0.
Bajo el modelo de memoria Java, los problemas de reordenamiento conducirán a tales problemas de visibilidad de la memoria. Según el modelo de memoria Java, cada hilo tiene su propia memoria de trabajo (principalmente el caché o el registro de la CPU), y sus operaciones en variables se llevan a cabo en su propia memoria de trabajo, mientras que la comunicación entre los subprocesos se logra a través de la sincronización entre la memoria principal y la memoria de trabajo de los subprocesos.
Por ejemplo, para el ejemplo anterior, el hilo de escritura ha actualizado con éxito el número a 42 y está listo para verdadero, pero es muy probable que el hilo de escritura solo sincronice el número de la memoria principal (tal vez debido al búfer de escritura de la CPU), lo que resulta en el valor listo que lee los hilos de lectura posteriores que siempre son falsos, por lo que el código anterior no generará ningún valor numérico.
Si usamos la palabra clave sincronizada para sincronizar, no habrá tal problema.
clase pública Novisibility {private static boolean Ready = false; Número de intst intivit intivate = 0; bloqueo de objeto estático privado = nuevo objeto (); REDET de la clase estática privada extiende el hilo {@Override public void run () {SynChronized (Lock) {while (! Ready) {thread.yield (); } System.out.println (número); }} public static void main (string [] args) {sincronizado (bloqueo) {new ReaderThread (). Start (); número = 42; Ready = True; }}} Esto se debe a que el modelo de memoria Java proporciona las siguientes garantías para la semántica sincronizada.
Es decir, cuando Thinda libera el bloqueo M, las variables que ha escrito (como X e Y, que están presentes en su memoria de trabajo) se sincronizarán con la memoria principal. Cuando Threadb se aplica para el mismo bloqueo M, la memoria de trabajo de Threadb se establecerá en Invalid, y luego Threadb recargará la variable a la que desea acceder desde la memoria principal a su memoria de trabajo (en este momento, x = 1, y = 1, es el último valor modificado en Thura). De esta manera, se logra la comunicación entre hilos de Thintera a Threadb.
Esta es en realidad una de las reglas de sucesión definidas por JSR133. JSR133 Define el siguiente conjunto de reglas de sucesión para el modelo de memoria Java.
De hecho, este conjunto de reglas de sucesión antes define la visibilidad de la memoria entre las operaciones. Si A opera ocurre antes de la operación B, entonces el resultado de ejecución de una operación (como escribir en variables) debe ser visible al realizar la operación B.
Para obtener una comprensión más profunda de estas reglas, tomemos un ejemplo:
// Código compartido por el hilo A y B Object Blok = New Object (); int a = 0; int b = 0; int c = 0; // hilo A, llame al siguiente código sincronizado (bloqueo) {a = 1; // 1 b = 2; // 2} // 3C = 3; // 4 // hilo B, llame al siguiente código sincronizado (bloqueo) {// 5 System.out.println (a); // 6 System.out.println (b); // 7 System.out.println (c); // 8}Suponemos que el hilo A se ejecuta primero, asigna valores a las tres variables A, B y C respectivamente (nota: la asignación de las variables A, B se realiza en el bloque de instrucciones sincrónicas), y luego el subproceso B vuelve a ejecutar, leyendo los valores de estas tres variables e imprimiendo. Entonces, ¿cuáles son los valores de las variables A, B y C imprimidos por el hilo B?
De acuerdo con la regla de subproceso único, en la ejecución del subproceso A, podemos obtener que 1 operación ocurra antes de 2 operaciones, 2 operaciones ocurren antes de 3 operaciones y 3 operaciones ocurren antes de 4 operaciones. Del mismo modo, en la ejecución del hilo B, 5 operaciones ocurren antes de 6 operaciones, 6 operaciones ocurren antes de 7 operaciones y 7 operaciones ocurren antes de 8 operaciones. De acuerdo con los principios de desbloqueo y bloqueo del monitor, las 3 operaciones (operación de desbloqueo) ocurren antes de 5 operaciones (operación de bloqueo). De acuerdo con las reglas transitivas, podemos concluir que las operaciones 1 y 2 ocurren antes de las operaciones 6, 7 y 8.
De acuerdo con la semántica de memoria de Herened antes, los resultados de ejecución de las operaciones 1 y 2 son visibles para las operaciones 6, 7 y 8, por lo que en el subproceso B, A y B se imprimen 1 y 2. Para las operaciones 4 y la operación 8 de la variable c. No podemos deducir la operación 4 ocurre antes de la Operación 8 de acuerdo con las reglas existentes antes. Por lo tanto, en el hilo B, la variable a la que se accede a C puede ser 0, no 3.