CodeGym /Blog Java /Random-ES /Mejor juntos: Java y la clase Thread. Parte II — Sincroni...
John Squirrels
Nivel 41
San Francisco

Mejor juntos: Java y la clase Thread. Parte II — Sincronización

Publicado en el grupo Random-ES

Introducción

Entonces, sabemos que Java tiene hilos. Puede leer sobre eso en la revisión titulada Mejor juntos: Java y la clase Thread. Parte I — Hilos de ejecución . Los hilos son necesarios para realizar el trabajo en paralelo. Esto hace que sea muy probable que los subprocesos interactúen de alguna manera entre sí. Veamos cómo sucede esto y qué herramientas básicas tenemos. Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 1

Producir

Thread.yield() es desconcertante y rara vez se usa. Se describe de muchas maneras diferentes en Internet. Incluyendo algunas personas que escriben que hay una cola de subprocesos, en la que descenderá un subproceso en función de las prioridades del subproceso. Otras personas escriben que un subproceso cambiará su estado de "En ejecución" a "Ejecutable" (aunque no hay distinción entre estos estados, es decir, Java no los distingue). La realidad es que todo es mucho menos conocido y, sin embargo, más simple en cierto sentido. Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 2Hay un error ( JDK-6416721: (subproceso de especificación) Fix Thread.yield() javadoc ) registrado para la yield()documentación del método. Si lo lees, es claro que elyield()El método en realidad solo proporciona alguna recomendación al programador de subprocesos de Java de que a este subproceso se le puede dar menos tiempo de ejecución. Pero lo que sucede realmente, es decir, si el programador actúa según la recomendación y lo que hace en general, depende de la implementación de la JVM y del sistema operativo. Y puede depender de algunos otros factores también. Lo más probable es que toda la confusión se deba al hecho de que los subprocesos múltiples se han replanteado a medida que se desarrolla el lenguaje Java. Lea más en la descripción general aquí: Breve introducción a Java Thread.yield() .

Dormir

Un hilo puede ir a dormir durante su ejecución. Este es el tipo más fácil de interacción con otros subprocesos. El sistema operativo que ejecuta la máquina virtual Java que ejecuta nuestro código Java tiene su propio planificador de subprocesos . Decide qué subproceso iniciar y cuándo. Un programador no puede interactuar con este programador directamente desde el código Java, solo a través de la JVM. Él o ella puede pedirle al programador que pause el hilo por un tiempo, es decir, que lo ponga a dormir. Puede leer más en estos artículos: Thread.sleep() y Cómo funciona Multithreading . También puede consultar cómo funcionan los subprocesos en los sistemas operativos Windows: Internals of Windows Thread . Y ahora vamos a verlo con nuestros propios ojos. Guarde el siguiente código en un archivo llamado HelloWorldApp.java:

class HelloWorldApp {
    public static void main(String []args) {
        Runnable task = () -> {
            try {
                int secToWait = 1000 * 60;
                Thread.currentThread().sleep(secToWait);
                System.out.println("Woke up");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(task);
        thread.start();
    }
}
Como puede ver, tenemos una tarea que espera 60 segundos, después de lo cual finaliza el programa. Compilamos usando el comando " javac HelloWorldApp.java" y luego ejecutamos el programa usando " java HelloWorldApp". Es mejor iniciar el programa en una ventana separada. Por ejemplo, en Windows, es así: start java HelloWorldApp. Usamos el comando jps para obtener el PID (ID del proceso), y abrimos la lista de subprocesos con " jvisualvm --openpid pid: Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 3Como puede ver, nuestro subproceso ahora tiene el estado "Durmiendo". De hecho, hay una forma más elegante de ayudar nuestro hilo tiene dulces sueños:

try {
	TimeUnit.SECONDS.sleep(60);
	System.out.println("Woke up");
} catch (InterruptedException e) {
	e.printStackTrace();
}
¿Te diste cuenta que estamos manejando InterruptedExceptionpor todos lados? Entendamos por qué.

Subproceso.interrupción()

La cuestión es que mientras un hilo está esperando/durmiendo, alguien puede querer interrumpir. En este caso, manejamos un InterruptedException. Este mecanismo se creó después de que el Thread.stop()método se declarara en desuso, es decir, obsoleto e indeseable. La razón fue que cuando stop()se llamó al método, el subproceso simplemente se "eliminó", lo cual era muy impredecible. No podíamos saber cuándo se detendría el subproceso y no podíamos garantizar la coherencia de los datos. Imagine que está escribiendo datos en un archivo mientras se elimina el hilo. En lugar de eliminar el hilo, los creadores de Java decidieron que sería más lógico decirle que debería interrumpirse. Cómo responder a esta información es un asunto que debe decidir el propio hilo. Para obtener más detalles, lea ¿Por qué Thread.stop está en desuso?en el sitio web de Oracle. Veamos un ejemplo:

public static void main(String []args) {
	Runnable task = () -> {
		try {
			TimeUnit.SECONDS.sleep(60);
		} catch (InterruptedException e) {
			System.out.println("Interrupted");
		}
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.interrupt();
}
En este ejemplo, no esperaremos 60 segundos. En su lugar, mostraremos inmediatamente "Interrumpido". Esto se debe a que llamamos al interrupt()método en el hilo. Este método establece un indicador interno llamado "estado de interrupción". Es decir, cada subproceso tiene una bandera interna a la que no se puede acceder directamente. Pero tenemos métodos nativos para interactuar con esta bandera. Pero esa no es la única manera. Un subproceso puede estar ejecutándose, no esperando algo, simplemente realizando acciones. Pero puede anticipar que otros querrán terminar su trabajo en un momento específico. Por ejemplo:

public static void main(String []args) {
	Runnable task = () -> {
		while(!Thread.currentThread().isInterrupted()) {
			// Do some work
		}
		System.out.println("Finished");
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.interrupt();
}
En el ejemplo anterior, el whileciclo se ejecutará hasta que el hilo se interrumpa externamente. En cuanto a la isInterruptedbandera, es importante saber que si capturamos un InterruptedException, la bandera isInterrupted se reinicia y luego isInterrupted()devolverá falso. La clase Thread también tiene un método estático Thread.interrupted() que se aplica solo al hilo actual, ¡pero este método restablece el indicador a falso! Obtenga más información en este capítulo titulado Interrupción de subprocesos .

Unirse (Esperar a que termine otro hilo)

El tipo más simple de espera es esperar a que termine otro subproceso.

public static void main(String []args) throws InterruptedException {
	Runnable task = () -> {
		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			System.out.println("Interrupted");
		}
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.join();
	System.out.println("Finished");
}
En este ejemplo, el nuevo hilo dormirá 5 segundos. Al mismo tiempo, el subproceso principal esperará hasta que el subproceso durmiente se despierte y termine su trabajo. Si observa el estado del subproceso en JVisualVM, se verá así: Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 4Gracias a las herramientas de monitoreo, puede ver lo que sucede con el subproceso. El joinmétodo es bastante simple, porque es solo un método con código Java que se ejecuta wait()mientras el subproceso en el que se llama esté vivo. Tan pronto como el hilo muere (cuando ha terminado con su trabajo), la espera se interrumpe. Y esa es toda la magia del join()método. Entonces, pasemos a lo más interesante.

Monitor

Multithreading incluye el concepto de un monitor. La palabra monitor llega al inglés a través del latín del siglo XVI y significa "un instrumento o dispositivo utilizado para observar, verificar o mantener un registro continuo de un proceso". En el contexto de este artículo, intentaremos cubrir los conceptos básicos. Para cualquiera que quiera los detalles, sumérjase en los materiales vinculados. Comenzamos nuestro viaje con la especificación del lenguaje Java (JLS): 17.1. Sincronización . Dice lo siguiente: Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 5Resulta que Java utiliza un mecanismo de "supervisión" para la sincronización entre subprocesos. Un monitor está asociado con cada objeto, y los subprocesos pueden adquirirlo lock()o liberarlo con unlock(). A continuación, encontraremos el tutorial en el sitio web de Oracle: Intrinsic Locks and Synchronization. Este tutorial dice que la sincronización de Java se basa en una entidad interna llamada bloqueo intrínseco o bloqueo de monitor . Este bloqueo a menudo se denomina simplemente " monitor ". También vemos nuevamente que cada objeto en Java tiene un bloqueo intrínseco asociado. Puede leer Java: bloqueos intrínsecos y sincronización . A continuación, será importante comprender cómo se puede asociar un objeto en Java con un monitor. En Java, cada objeto tiene un encabezado que almacena metadatos internos que no están disponibles para el programador desde el código, pero que la máquina virtual necesita para funcionar correctamente con los objetos. El encabezado del objeto incluye una "palabra de marca", que se ve así: Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

Aquí hay un artículo de JavaWorld que es muy útil: Cómo la máquina virtual Java realiza la sincronización de subprocesos . Este artículo debe combinarse con la descripción de la sección "Resumen" del siguiente problema en el sistema de seguimiento de errores de JDK: JDK-8183909 . Puede leer lo mismo aquí: JEP-8183909 . Entonces, en Java, un monitor está asociado con un objeto y se usa para bloquear un subproceso cuando el subproceso intenta adquirir (u obtener) el bloqueo. Aquí está el ejemplo más simple:

public class HelloWorld{
    public static void main(String []args){
        Object object = new Object();
        synchronized(object) {
            System.out.println("Hello World");
        }
    }
}
Aquí, el subproceso actual (aquel en el que se ejecutan estas líneas de código) usa la synchronizedpalabra clave para intentar usar el monitor asociado con elobject"\variable para obtener/adquirir el bloqueo. Si nadie más está compitiendo por el monitor (es decir, nadie más está ejecutando código sincronizado usando el mismo objeto), entonces Java puede intentar realizar una optimización llamada "bloqueo sesgado". Una etiqueta relevante y un registro sobre qué subproceso posee el bloqueo del monitor se agregan a la palabra de marca en el encabezado del objeto. Esto reduce la sobrecarga necesaria para bloquear un monitor. Si el monitor pertenecía anteriormente a otro subproceso, dicho bloqueo no es suficiente. La JVM cambia al siguiente tipo de bloqueo: "bloqueo básico". Utiliza operaciones de comparación e intercambio (CAS). Además, la palabra de marca del encabezado del objeto ya no almacena la palabra de marca, sino una referencia a dónde está almacenada, y la etiqueta cambia para que la JVM entienda que estamos usando un bloqueo básico. Si varios subprocesos compiten (contienden) por un monitor (uno ha adquirido el bloqueo y el segundo está esperando que se libere), la etiqueta en la palabra de marca cambia y la palabra de marca ahora almacena una referencia al monitor. como un objeto: alguna entidad interna de la JVM. Como se indica en la Propuesta de mejora de JDK (JEP), esta situación requiere espacio en el área de memoria de Native Heap para almacenar esta entidad. La referencia a la ubicación de memoria de esta entidad interna se almacenará en la palabra de marca del encabezado del objeto. Por lo tanto, un monitor es realmente un mecanismo para sincronizar el acceso a los recursos compartidos entre varios subprocesos. La JVM cambia entre varias implementaciones de este mecanismo. Entonces, para simplificar, cuando hablamos del monitor, en realidad estamos hablando de bloqueos. y un segundo está esperando que se libere el bloqueo), luego la etiqueta en la palabra de marca cambia, y la palabra de marca ahora almacena una referencia al monitor como un objeto, alguna entidad interna de la JVM. Como se indica en la Propuesta de mejora de JDK (JEP), esta situación requiere espacio en el área de memoria de Native Heap para almacenar esta entidad. La referencia a la ubicación de memoria de esta entidad interna se almacenará en la palabra de marca del encabezado del objeto. Por lo tanto, un monitor es realmente un mecanismo para sincronizar el acceso a los recursos compartidos entre varios subprocesos. La JVM cambia entre varias implementaciones de este mecanismo. Entonces, para simplificar, cuando hablamos del monitor, en realidad estamos hablando de bloqueos. y un segundo está esperando que se libere el bloqueo), luego la etiqueta en la palabra de marca cambia, y la palabra de marca ahora almacena una referencia al monitor como un objeto, alguna entidad interna de la JVM. Como se indica en la Propuesta de mejora de JDK (JEP), esta situación requiere espacio en el área de memoria de Native Heap para almacenar esta entidad. La referencia a la ubicación de memoria de esta entidad interna se almacenará en la palabra de marca del encabezado del objeto. Por lo tanto, un monitor es realmente un mecanismo para sincronizar el acceso a los recursos compartidos entre varios subprocesos. La JVM cambia entre varias implementaciones de este mecanismo. Entonces, para simplificar, cuando hablamos del monitor, en realidad estamos hablando de bloqueos. y la palabra de marca ahora almacena una referencia al monitor como un objeto, alguna entidad interna de la JVM. Como se indica en la Propuesta de mejora de JDK (JEP), esta situación requiere espacio en el área de memoria de Native Heap para almacenar esta entidad. La referencia a la ubicación de memoria de esta entidad interna se almacenará en la palabra de marca del encabezado del objeto. Por lo tanto, un monitor es realmente un mecanismo para sincronizar el acceso a los recursos compartidos entre varios subprocesos. La JVM cambia entre varias implementaciones de este mecanismo. Entonces, para simplificar, cuando hablamos del monitor, en realidad estamos hablando de bloqueos. y la palabra de marca ahora almacena una referencia al monitor como un objeto, alguna entidad interna de la JVM. Como se indica en la Propuesta de mejora de JDK (JEP), esta situación requiere espacio en el área de memoria de Native Heap para almacenar esta entidad. La referencia a la ubicación de memoria de esta entidad interna se almacenará en la palabra de marca del encabezado del objeto. Por lo tanto, un monitor es realmente un mecanismo para sincronizar el acceso a los recursos compartidos entre varios subprocesos. La JVM cambia entre varias implementaciones de este mecanismo. Entonces, para simplificar, cuando hablamos del monitor, en realidad estamos hablando de bloqueos. La referencia a la ubicación de memoria de esta entidad interna se almacenará en la palabra de marca del encabezado del objeto. Por lo tanto, un monitor es realmente un mecanismo para sincronizar el acceso a los recursos compartidos entre varios subprocesos. La JVM cambia entre varias implementaciones de este mecanismo. Entonces, para simplificar, cuando hablamos del monitor, en realidad estamos hablando de bloqueos. La referencia a la ubicación de memoria de esta entidad interna se almacenará en la palabra de marca del encabezado del objeto. Por lo tanto, un monitor es realmente un mecanismo para sincronizar el acceso a los recursos compartidos entre varios subprocesos. La JVM cambia entre varias implementaciones de este mecanismo. Entonces, para simplificar, cuando hablamos del monitor, en realidad estamos hablando de bloqueos. Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 7

Sincronizado (esperando un bloqueo)

Como vimos anteriormente, el concepto de "bloque sincronizado" (o "sección crítica") está estrechamente relacionado con el concepto de monitor. Echa un vistazo a un ejemplo:

public static void main(String[] args) throws InterruptedException {
	Object lock = new Object();

	Runnable task = () -> {
		synchronized(lock) {
			System.out.println("thread");
		}
	};

	Thread th1 = new Thread(task);
	th1.start();
	synchronized(lock) {
		for (int i = 0; i < 8; i++) {
			Thread.currentThread().sleep(1000);
			System.out.print(" " + i);
		}
		System.out.println(" ...");
	}
}
Aquí, el subproceso principal primero pasa el objeto de la tarea al nuevo subproceso y luego adquiere inmediatamente el bloqueo y realiza una operación larga con él (8 segundos). Durante todo este tiempo, la tarea no puede continuar, porque no puede ingresar al synchronizedbloque, porque el bloqueo ya está adquirido. Si el subproceso no puede obtener el bloqueo, esperará al monitor. Tan pronto como obtenga el bloqueo, continuará la ejecución. Cuando un subproceso sale de un monitor, libera el bloqueo. En JVisualVM, se ve así: Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 8como puede ver en JVisualVM, el estado es "Monitor", lo que significa que el subproceso está bloqueado y no puede tomar el monitor. También puede usar código para determinar el estado de un subproceso, pero los nombres de estado determinados de esta manera no coinciden con los nombres usados ​​en JVisualVM, aunque son similares. En este caso, elth1.getState()declaración en el ciclo for devolverá BLOCKED , porque mientras el ciclo se esté ejecutando, el lockmonitor del objeto está ocupado por el mainhilo, y el th1hilo está bloqueado y no puede continuar hasta que se libere el bloqueo. Además de los bloques sincronizados, se puede sincronizar un método completo. Por ejemplo, aquí hay un método de la HashTableclase:

public synchronized int size() {
	return count;
}
Este método será ejecutado por un solo subproceso en un momento dado. ¿Realmente necesitamos la cerradura? Sí, lo necesitamos. En el caso de los métodos de instancia, el objeto "este" (objeto actual) actúa como un candado. Hay una discusión interesante sobre este tema aquí: ¿ Hay alguna ventaja en usar un método sincronizado en lugar de un bloque sincronizado? . Si el método es estático, entonces el bloqueo no será el objeto "este" (porque no hay ningún objeto "este" para un método estático), sino un objeto Clase (por ejemplo, ) Integer.class.

Espere (esperando un monitor). métodos notificar () y notificar a todos ()

La clase Thread tiene otro método de espera asociado con un monitor. A diferencia de sleep()y join(), este método no se puede llamar simplemente. Su nombre es wait(). El waitmétodo se llama sobre el objeto asociado al monitor que queremos esperar. Veamos un ejemplo:

public static void main(String []args) throws InterruptedException {
	    Object lock = new Object();
	    // The task object will wait until it is notified via lock
	    Runnable task = () -> {
	        synchronized(lock) {
	            try {
	                lock.wait();
	            } catch(InterruptedException e) {
	                System.out.println("interrupted");
	            }
	        }
	        // After we are notified, we will wait until we can acquire the lock
	        System.out.println("thread");
	    };
	    Thread taskThread = new Thread(task);
	    taskThread.start();
        // We sleep. Then we acquire the lock, notify, and release the lock
	    Thread.currentThread().sleep(3000);
	    System.out.println("main");
	    synchronized(lock) {
	        lock.notify();
	    }
}
En JVisualVM, se ve así: Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 10Para comprender cómo funciona esto, recuerde que los métodos wait()y notify()están asociados con java.lang.Object. Puede parecer extraño que los métodos relacionados con subprocesos estén en la Objectclase. Pero la razón de eso ahora se revela. Recordará que cada objeto en Java tiene un encabezado. El encabezado contiene diversa información de mantenimiento, incluida información sobre el monitor, es decir, el estado de la cerradura. Recuerde, cada objeto, o instancia de una clase, está asociado con una entidad interna en la JVM, denominada bloqueo intrínseco o monitor. En el ejemplo anterior, el código para el objeto de la tarea indica que ingresamos el bloque sincronizado para el monitor asociado con el lockobjeto. Si logramos adquirir el bloqueo para este monitor, entonceswait()se llama. El subproceso que ejecuta la tarea liberará el lockmonitor del objeto, pero ingresará a la cola de subprocesos que esperan la notificación del lockmonitor del objeto. Esta cola de subprocesos se llama WAIT SET, que refleja más adecuadamente su propósito. Es decir, es más un conjunto que una cola. El mainsubproceso crea un nuevo subproceso con el objeto de la tarea, lo inicia y espera 3 segundos. Esto hace que sea muy probable que el nuevo subproceso pueda adquirir el bloqueo antes que el mainsubproceso y entrar en la cola del monitor. Después de eso, el mainsubproceso mismo ingresa al lockbloque sincronizado del objeto y realiza la notificación del subproceso utilizando el monitor. Después de enviar la notificación, el mainsubproceso libera ellockmonitor del objeto, y el nuevo subproceso, que anteriormente estaba esperando que lockse liberara el monitor del objeto, continúa la ejecución. Es posible enviar una notificación a un solo hilo ( notify()) o simultáneamente a todos los hilos en la cola ( notifyAll()). Lea más aquí: Diferencia entre notificar () y notificar a todos () en Java . Es importante tener en cuenta que el orden de notificación depende de cómo se implemente la JVM. Lea más aquí: ¿ Cómo resolver el hambre con notificar y notificar a todos? . La sincronización se puede realizar sin especificar un objeto. Puede hacer esto cuando se sincroniza un método completo en lugar de un solo bloque de código. Por ejemplo, para métodos estáticos, el bloqueo será un objeto Class (obtenido a través de .class):

public static synchronized void printA() {
	System.out.println("A");
}
public static void printB() {
	synchronized(HelloWorld.class) {
		System.out.println("B");
	}
}
En términos de uso de bloqueos, ambos métodos son iguales. Si un método no es estático, la sincronización se realizará utilizando el método actual instance, es decir, utilizando this. Por cierto, dijimos anteriormente que puede usar el getState()método para obtener el estado de un hilo. Por ejemplo, para un subproceso en la cola que espera un monitor, el estado será ESPERANDO o TIMED_WAITING, si el wait()método especificó un tiempo de espera. Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 11

https://stackoverflow.com/questions/36425942/cuál-es-el-ciclo-de-vida-de-thread-in-java

Ciclo de vida del hilo

A lo largo de su vida, el estado de un hilo cambia. De hecho, estos cambios comprenden el ciclo de vida del subproceso. Tan pronto como se crea un hilo, su estado es NUEVO. En este estado, el nuevo subproceso aún no se está ejecutando y el programador de subprocesos de Java aún no sabe nada al respecto. Para que el programador de subprocesos aprenda sobre el subproceso, debe llamar al thread.start()método. Luego, el subproceso pasará al estado EJECUTABLE. Internet tiene muchos diagramas incorrectos que distinguen entre los estados "Ejecutable" y "En ejecución". Pero esto es un error, porque Java no distingue entre "listo para funcionar" (ejecutable) y "funcionando" (en ejecución). Cuando un subproceso está vivo pero no activo (no ejecutable), se encuentra en uno de dos estados:
  • BLOQUEADO: esperando entrar en una sección crítica, es decir, un synchronizedbloque.
  • ESPERANDO: esperando que otro subproceso satisfaga alguna condición.
Si se cumple la condición, el programador de subprocesos inicia el subproceso. Si el subproceso está esperando hasta un tiempo específico, entonces su estado es TIMED_WAITING. Si el subproceso ya no se está ejecutando (está terminado o se lanzó una excepción), entonces ingresa al estado TERMINADO. Para averiguar el estado de un subproceso, utilice el getState()método. Los subprocesos también tienen un isAlive()método, que devuelve verdadero si el subproceso no está TERMINADO.

LockSupport y estacionamiento de hilos

A partir de Java 1.6, apareció un mecanismo interesante llamado LockSupport . Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 12Esta clase asocia un "permiso" con cada hilo que lo usa. Una llamada al park()método regresa inmediatamente si el permiso está disponible, consumiendo el permiso en el proceso. De lo contrario, se bloquea. Llamar al unparkmétodo hace que el permiso esté disponible si aún no está disponible. Solo hay 1 permiso. La documentación de Java para LockSupportse refiere a la Semaphoreclase. Veamos un ejemplo sencillo:

import java.util.concurrent.Semaphore;
public class HelloWorldApp{
    
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(0);
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            // Request the permit and wait until we get it
            e.printStackTrace();
        }
        System.out.println("Hello, World!");
    }
}
Este código siempre esperará, porque ahora el semáforo tiene 0 permisos. Y cuando acquire()se llama en el código (es decir, solicita el permiso), el subproceso espera hasta que recibe el permiso. Ya que estamos esperando, debemos manejar InterruptedException. Curiosamente, el semáforo obtiene un estado de hilo separado. Si miramos en JVisualVM, veremos que el estado no es "Esperar", sino "Estacionar". Mejor juntos: Java y la clase Thread.  Parte II — Sincronización - 13Veamos otro ejemplo:

public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            // Park the current thread
            System.err.println("Will be Parked");
            LockSupport.park();
            // As soon as we are unparked, we will start to act
            System.err.println("Unparked");
        };
        Thread th = new Thread(task);
        th.start();
        Thread.currentThread().sleep(2000);
        System.err.println("Thread state: " + th.getState());
        
        LockSupport.unpark(th);
        Thread.currentThread().sleep(2000);
}
El estado del hilo será ESPERANDO, pero JVisualVM distingue entre waitla synchronizedpalabra clave y parkla LockSupportclase. ¿Por qué es esto LockSupporttan importante? Volvemos nuevamente a la documentación de Java y observamos el estado del subproceso EN ESPERA . Como puede ver, solo hay tres formas de ingresar. Dos de esas formas son wait()y join(). Y el tercero es LockSupport. En Java, los bloqueos también se pueden construir en LockSupport y ofrecer herramientas de nivel superior. Intentemos usar uno. Por ejemplo, eche un vistazo a ReentrantLock:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{

    public static void main(String []args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Runnable task = () -> {
            lock.lock();
            System.out.println("Thread");
            lock.unlock();
        };
        lock.lock();

        Thread th = new Thread(task);
        th.start();
        System.out.println("main");
        Thread.currentThread().sleep(2000);
        lock.unlock();
    }
}
Al igual que en los ejemplos anteriores, todo es simple aquí. El lockobjeto espera a que alguien libere el recurso compartido. Si miramos en JVisualVM, veremos que el nuevo subproceso estará estacionado hasta que el mainsubproceso libere el bloqueo. Puede leer más sobre bloqueos aquí: Java 8 StampedLocks vs. ReadWriteLocks y Synchronized and Lock API en Java. Para comprender mejor cómo se implementan los bloqueos, es útil leer sobre Phaser en este artículo: Guía de Java Phaser . Y hablando de varios sincronizadores, debe leer el artículo de DZone sobre The Java Synchronizers.

Conclusión

En esta revisión, examinamos las principales formas en que los subprocesos interactúan en Java. Material adicional: Mejor juntos: Java y la clase Thread. Parte I — Hilos de ejecución Mejor juntos: Java y la clase Thread. Parte III — Interacción Mejor juntos: Java y la clase Thread. Parte IV — Callable, Future y amigos Mejor juntos: Java y la clase Thread. Parte V — Ejecutor, ThreadPool, Fork/Join Mejor juntos: Java y la clase Thread. Parte VI — ¡Dispara!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION