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

Mejor juntos: Java y la clase Thread. Parte VI — ¡Dispara!

Publicado en el grupo Random-ES

Introducción

Los hilos son algo interesante. En revisiones anteriores, analizamos algunas de las herramientas disponibles para implementar subprocesos múltiples. Veamos qué otras cosas interesantes podemos hacer. En este punto, sabemos mucho. Por ejemplo, de " Mejor juntos: Java y la clase Thread. Part I — Threads of operating ", sabemos que la clase Thread representa un hilo de ejecución. Sabemos que un hilo realiza alguna tarea. Si queremos que nuestras tareas puedan hacerlo run, entonces debemos marcar el hilo con Runnable. Mejor juntos: Java y la clase Thread.  Parte VI — ¡Dispara!  - 1Para recordar, podemos usar el Compilador de Java en línea de Tutorialspoint :

public static void main(String[] args){
	Runnable task = () -> {
 		Thread thread = Thread.currentThread();
		System.out.println("Hello from " + thread.getName());
	};
	Thread thread = new Thread(task);
	thread.start();
}
También sabemos que tenemos algo llamado candado. Aprendimos sobre esto en " Mejor juntos: Java y la clase Thread. Parte II — Sincronización . Si un subproceso adquiere un bloqueo, entonces otro subproceso que intente adquirir el bloqueo se verá obligado a esperar a que se libere el bloqueo:

import java.util.concurrent.locks.*;

public class HelloWorld{
	public static void main(String []args){
		Lock lock = new ReentrantLock();
		Runnable task = () -> {
			lock.lock();
			Thread thread = Thread.currentThread();
			System.out.println("Hello from " + thread.getName());
			lock.unlock();
		};
		Thread thread = new Thread(task);
		thread.start();
	}
}
Creo que es hora de hablar sobre qué otras cosas interesantes podemos hacer.

Semáforos

La forma más sencilla de controlar cuántos subprocesos se pueden ejecutar simultáneamente es un semáforo. Es como una señal de tren. Verde significa proceder. Rojo significa esperar. ¿Esperar qué del semáforo? Acceso. Para obtener acceso, debemos adquirirlo. Y cuando ya no se necesita el acceso, debemos regalarlo o liberarlo. Veamos cómo funciona esto. Necesitamos importar la java.util.concurrent.Semaphoreclase. Ejemplo:

public static void main(String[] args) throws InterruptedException {
	Semaphore semaphore = new Semaphore(0);
	Runnable task = () -> {
		try {
			semaphore.acquire();
			System.out.println("Finished");
			semaphore.release();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	new Thread(task).start();
	Thread.sleep(5000);
	semaphore.release(1);
}
Como puede ver, estas operaciones (adquirir y liberar) nos ayudan a comprender cómo funciona un semáforo. Lo más importante es que si queremos acceder, entonces el semáforo debe tener un número positivo de permisos. Esta cuenta se puede inicializar a un número negativo. Y podemos solicitar (adquirir) más de 1 permiso.

pestillo de cuenta regresiva

El siguiente mecanismo es CountDownLatch. Como era de esperar, este es un pestillo con una cuenta regresiva. Aquí necesitamos la declaración de importación apropiada para la java.util.concurrent.CountDownLatchclase. Es como una carrera a pie, donde todos se reúnen en la línea de salida. Y una vez que todos están listos, todos reciben la señal de inicio al mismo tiempo y comienzan simultáneamente. Ejemplo:

public static void main(String[] args) {
	CountDownLatch countDownLatch = new CountDownLatch(3);
	Runnable task = () -> {
		try {
			countDownLatch.countDown();
			System.out.println("Countdown: " + countDownLatch.getCount());
			countDownLatch.await();
			System.out.println("Finished");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	for (int i = 0; i < 3; i++) {
		new Thread(task).start();
 	}
}
Primero, primero le decimos al pestillo que countDown(). Google define la cuenta regresiva como "un acto de contar números en orden inverso a cero". Y luego le decimos al pestillo que await(), es decir, espere hasta que el contador se ponga a cero. Curiosamente, este es un contador de una sola vez. La documentación de Java dice: "Cuando los subprocesos deben contar repetidamente de esta manera, en su lugar, use CyclicBarrier". En otras palabras, si necesita un contador reutilizable, necesita una opción diferente: CyclicBarrier.

Barrera cíclica

Como su nombre lo indica, CyclicBarrieres una barrera "reutilizable". Tendremos que importar la java.util.concurrent.CyclicBarrierclase. Veamos un ejemplo:

public static void main(String[] args) throws InterruptedException {
	Runnable action = () -> System.out.println("On your mark!");
	CyclicBarrier barrier = new CyclicBarrier(3, action);
	Runnable task = () -> {
		try {
			barrier.await();
			System.out.println("Finished");
		} catch (BrokenBarrierException | InterruptedException e) {
			e.printStackTrace();
		}
	};
	System.out.println("Limit: " + barrier.getParties());
	for (int i = 0; i < 3; i++) {
		new Thread(task).start();
	}
}
Como puede ver, el hilo ejecuta el awaitmétodo, es decir, espera. En este caso, el valor de la barrera disminuye. La barrera se considera rota ( barrier.isBroken()) cuando la cuenta atrás llega a cero. Para restablecer la barrera, debe llamar al reset()método, que CountDownLatchno tiene.

intercambiador

El siguiente mecanismo es Exchanger. En este contexto, un intercambio es un punto de sincronización donde las cosas cambian, se intercambian o intercambian. Como era de esperar, an Exchangeres una clase que realiza un intercambio o permuta. Veamos el ejemplo más simple:

public static void main(String[] args) {
	Exchanger<String> exchanger = new Exchanger<>();
	Runnable task = () -> {
		try {
			Thread thread = Thread.currentThread();
			String withThreadName = exchanger.exchange(thread.getName());
			System.out.println(thread.getName() + " exchanged with " + withThreadName);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	new Thread(task).start();
	new Thread(task).start();
}
Aquí empezamos dos hilos. Cada uno de ellos ejecuta el método de intercambio y espera a que el otro hilo también ejecute el método de intercambio. Al hacerlo, los subprocesos intercambian los argumentos pasados. Interesante. ¿No te recuerda a algo? Es una reminiscencia de SynchronousQueue, que se encuentra en el corazón de CachedThreadPool. Para mayor claridad, aquí hay un ejemplo:

public static void main(String[] args) throws InterruptedException {
	SynchronousQueue<String> queue = new SynchronousQueue<>();
	Runnable task = () -> {
		try {
			System.out.println(queue.take());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	new Thread(task).start();
	queue.put("Message");
}
El ejemplo muestra que cuando se inicia un nuevo hilo, esperará porque la cola estará vacía. Y luego el hilo principal pone la cadena "Mensaje" en la cola. Además, también se detendrá hasta que se reciba esta cadena de la cola. También puede leer " SynchronousQueue vs Exchanger " para obtener más información sobre este tema.

fáser

Hemos guardado lo mejor para el final: Phaser. Tendremos que importar la java.util.concurrent.Phaserclase. Veamos un ejemplo sencillo:

public static void main(String[] args) throws InterruptedException {
        Phaser phaser = new Phaser();
        // By calling the register method, we register the current (main) thread as a party
        phaser.register();
        System.out.println("Phasecount is " + phaser.getPhase());
        testPhaser(phaser);
        testPhaser(phaser);
        testPhaser(phaser);
        // After 3 seconds, we arrive at the barrier and deregister. Number of arrivals = number of registrations = start
        Thread.sleep(3000);
        phaser.arriveAndDeregister();
        System.out.println("Phasecount is " + phaser.getPhase());
    }

    private static void testPhaser(final Phaser phaser) {
        // We indicate that there will be a +1 party on the Phaser
        phaser.register();
        // Start a new thread
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            System.out.println(name + " arrived");
            phaser.arriveAndAwaitAdvance(); // The threads register arrival at the phaser.
            System.out.println(name + " after passing barrier");
        }).start();
    }
El ejemplo ilustra que cuando se usa Phaser, la barrera se rompe cuando el número de registros coincide con el número de llegadas a la barrera. Puede familiarizarse más Phaserleyendo este artículo de GeeksforGeeks .

Resumen

Como puede ver en estos ejemplos, hay varias formas de sincronizar subprocesos. Anteriormente, traté de recordar aspectos de subprocesos múltiples. Espero que las entregas anteriores de esta serie hayan sido útiles. Algunas personas dicen que el camino hacia los subprocesos múltiples comienza con el libro "Java Concurrency in Practice". Aunque se publicó en 2006, la gente dice que el libro es bastante fundamental y sigue siendo relevante hoy en día. Por ejemplo, puede leer la discusión aquí: ¿ Sigue siendo válida la "Simultaneidad de Java en la práctica"? . También es útil leer los enlaces en la discusión. Por ejemplo, hay un enlace al libro The Well-Grounded Java Developer , y haremos una mención particular al Capítulo 4. Concurrencia moderna . También hay una revisión completa sobre este tema:¿Sigue siendo válida la "concurrencia de Java en la práctica" en la era de Java 8? Ese artículo también ofrece sugerencias sobre qué más leer para comprender verdaderamente este tema. Después de eso, podría echar un vistazo a un gran libro como OCA/OCP Java SE 8 Programmer Practice Tests . Nos interesa el segundo acrónimo: OCP (Oracle Certified Professional). Encontrará pruebas en el "Capítulo 20: Simultaneidad de Java". Este libro tiene preguntas y respuestas con explicaciones. Por ejemplo: Mejor juntos: Java y la clase Thread.  Parte VI — ¡Dispara!  - 3Muchas personas podrían empezar a decir que esta pregunta es otro ejemplo más de memorización de métodos. Por un lado, sí. Por otro lado, podría responder a esta pregunta recordando que ExecutorServicees una especie de "actualización" de Executor. YExecutorpretende simplemente ocultar la forma en que se crean los subprocesos, pero no es la forma principal de ejecutarlos, es decir, iniciar un Runnableobjeto en un nuevo subproceso. Es por eso que no hay execute(Callable), porque en ExecutorService, Executorsimplemente agrega submit()métodos que pueden devolver un Futureobjeto. Por supuesto, podemos memorizar una lista de métodos, pero es mucho más fácil hacer nuestra respuesta basada en nuestro conocimiento de la naturaleza de las clases mismas. Y aquí hay algunos materiales adicionales sobre el tema: Mejor juntos: Java y la clase Thread. Parte I — Hilos de ejecución Mejor juntos: Java y la clase Thread. Parte II — Sincronizació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
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION