JavaRush /Blog Java /Random-ES /No puedes estropear Java con un hilo: Parte VI - ¡A la ba...
Viacheslav
Nivel 3

No puedes estropear Java con un hilo: Parte VI - ¡A la barrera!

Publicado en el grupo Random-ES

Introducción

Las transmisiones son algo interesante. En revisiones anteriores, analizamos algunas de las herramientas disponibles para implementar subprocesos múltiples. Veamos qué más cosas interesantes podemos hacer. A estas alturas sabemos mucho. Por ejemplo, de " No se puede estropear Java con un subproceso: Parte I - Subprocesos ", sabemos que un subproceso es un subproceso. Sabemos que un hilo está realizando alguna tarea. Si queremos que nuestra tarea pueda ejecutarse ( run), debemos especificar que el hilo sea un determinado Runnable. No puedes estropear Java con un hilo: Parte VI - ¡A la barrera!  - 1Para recordar, podemos usar el compilador en línea Java 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 el concepto de candado. Leemos sobre esto en " No se puede estropear Java con un hilo: Parte II - Sincronización ". Un hilo puede ocupar un candado y luego otro hilo que intente ocupar el candado se verá obligado a esperar a que el candado se libere:
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é más podemos hacer que sea interesante.

Semáforos

El medio más sencillo para controlar cuántos subprocesos pueden funcionar simultáneamente es un semáforo. Como en el ferrocarril. La luz verde está encendida: puedes hacerlo. La luz roja está encendida: estamos esperando. ¿Qué esperamos de un semáforo? Permisos. Permiso en inglés - permiso. Para obtener el permiso es necesario obtenerlo, que en inglés se adquirirá. Y cuando ya no sea necesario el permiso, debemos regalarlo, es decir, liberarlo o deshacernos de él, que en inglés será liberar. Vamos a ver cómo funciona. Necesitaremos importar la clase java.util.concurrent.Semaphore. 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 podemos ver, después de memorizar palabras en inglés, entendemos cómo funciona el semáforo. Curiosamente, la condición principal es que la “cuenta” del semáforo debe tener un número positivo de permisos. Por lo tanto, puedes iniciarlo con un menos. Y puedes solicitar (adquirir) más de 1.

cuenta atrás

El siguiente mecanismo es CountDownLatch. CountDown en inglés es una cuenta regresiva y Latch es un perno o pestillo. Es decir, si lo traducimos, entonces es un pestillo con cuenta regresiva. Aquí necesitamos la importación apropiada de la clase java.util.concurrent.CountDownLatch. Es como una carrera o carrera donde todos se reúnen en la línea de salida y cuando todos están listos, se da permiso y todos comienzan al mismo tiempo. 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();
 	}
}
esperar en inglés - esperar. Es decir, hablamos primero countDown. Como dice Google Translator, la cuenta regresiva es "un acto de contar números en orden inverso hasta cero", es decir, realizar una acción de cuenta regresiva, cuyo propósito es contar hasta cero. Y luego decimos await, es decir, espere hasta que el valor del contador llegue a cero. Es interesante que dicho mostrador sea desechable. Como se dice en JavaDoc: "Cuando los subprocesos deben contar repetidamente de esta manera, use CyclicBarrier", es decir, si necesita un recuento reutilizable, debe usar otra opción, que se llama CyclicBarrier.

Barrera cíclica

Como sugiere el nombre, CyclicBarrieres una barrera cíclica. Necesitaremos importar la clase java.util.concurrent.CyclicBarrier. Veamos un ejemplo:
public static void main(String[] args) throws InterruptedException {
	Runnable action = () -> System.out.println("На старт!");
	CyclicBarrier berrier = new CyclicBarrier(3, action);
	Runnable task = () -> {
		try {
			berrier.await();
			System.out.println("Finished");
		} catch (BrokenBarrierException | InterruptedException e) {
			e.printStackTrace();
		}
	};
	System.out.println("Limit: " + berrier.getParties());
	for (int i = 0; i < 3; i++) {
		new Thread(task).start();
	}
}
Como puedes ver, el hilo se está ejecutando await, es decir, esperando. En este caso, el valor de la barrera disminuye. La barrera se considera rota ( berrier.isBroken()) cuando la cuenta atrás llega a cero. Para restablecer la barrera, es necesario llamar a berrier.reset(), que faltaba en CountDownLatch.

Intercambiador

El siguiente remedio es Exchanger. Exchange del inglés se traduce como intercambio o intercambio. A Exchangeres un intercambiador, es decir, algo a través de lo cual intercambian. Veamos un ejemplo sencillo:
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() + " обменялся с " + withThreadName);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	new Thread(task).start();
	new Thread(task).start();
}
Aquí lanzamos dos hilos. Cada uno de ellos ejecuta el método de intercambio y espera a que otro hilo también ejecute el método de intercambio. Así, los hilos intercambiarán los argumentos pasados ​​entre ellos. Cosa interesante. ¿No te recuerda a nada? Y recuerda SynchronousQueuelo que se encuentra en el corazón de cachedThreadPool'a. 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 al iniciar un nuevo hilo, este hilo entrará en modo de espera, porque la cola estará vacía. Y luego mainel hilo pondrá en cola el texto "Mensaje". Al mismo tiempo, se detendrá durante el tiempo requerido hasta que reciba este elemento de texto de la cola. Sobre este tema también puedes leer " SynchronousQueue Vs Exchanger ".

fáser

Y finalmente, lo más dulce Phaser... Necesitaremos importar la clase java.util.concurrent.Phaser. Veamos un ejemplo sencillo:
public static void main(String[] args) throws InterruptedException {
        Phaser phaser = new Phaser();
        // Вызывая метод register, мы регистрируем текущий поток (main) Cómo участника
        phaser.register();
        System.out.println("Phasecount is " + phaser.getPhase());
        testPhaser(phaser);
        testPhaser(phaser);
        testPhaser(phaser);
        // Через 3 секунды прибываем к барьеру и снимаемся регистрацию. Кол-во прибывших = кол-во регистраций = пуск
        Thread.sleep(3000);
        phaser.arriveAndDeregister();
        System.out.println("Phasecount is " + phaser.getPhase());
    }

    private static void testPhaser(final Phaser phaser) {
        // Говорим, что будет +1 участник на Phaser
        phaser.register();
        // Запускаем новый поток
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            System.out.println(name + " arrived");
            phaser.arriveAndAwaitAdvance(); //threads register arrival to the phaser.
            System.out.println(name + " after passing barrier");
        }).start();
    }
El ejemplo muestra que la barrera, al utilizar Phaser'a, se rompe cuando el número de registros coincide con el número de llegadas a la barrera. Puede obtener más información Phaseren el artículo del centro " Nuevo sincronizador Phaser ".

Resultados

Como puede ver en los ejemplos, existen diferentes formas de sincronizar subprocesos. Anteriormente intenté recordar algo sobre subprocesos múltiples, espero que las partes anteriores hayan sido útiles. Dicen que el camino hacia el subproceso múltiple comienza con el libro "Java Concurrency in Practice". Aunque salió a la luz en 2006, la gente responde que el libro es bastante fundamental y todavía tiene gran impacto. Por ejemplo, puede leer las discusiones aquí: "¿ Sigue siendo válida la concurrencia de Java en la práctica? ". También es útil leer los enlaces de la discusión. Por ejemplo, hay un enlace al libro " The Well-Grounded Java Developer ", donde vale la pena prestar atención al " Capítulo 4. Concurrencia moderna ". Hay otra reseña completa sobre el mismo tema: "¿ La cocurrencia de Java en la práctica sigue siendo relevante en la era de Java 8 ?". También tiene consejos sobre qué más debería leer para comprender realmente el tema. Después de eso, puede echar un vistazo más de cerca a un libro tan maravilloso como " Pruebas de práctica del programador OCA OCP JavaSE 8 ". Nos interesa la segunda parte, es decir, OCP. Y hay pruebas en "∫". Este libro contiene preguntas y respuestas con explicaciones. Por ejemplo: No puedes estropear Java con un hilo: Parte VI - ¡A la barrera!  - 3Muchos podrán empezar a decir que esto es sólo otra memorización de métodos. Por un lado, sí. Por otro lado, esta pregunta se puede responder recordando que ExecutorServicese trata de una especie de “actualización” Executor. Y Executorsu objetivo es simplemente ocultar el método de creación de hilos, pero no la forma principal de ejecutarlos, es decir, ejecutar en un hilo nuevo Runnable. Por lo tanto execute(Callable)no, porque simplemente agregaron métodos que ExecutorServicepueden regresar . Como puedes ver, podemos memorizar una lista de métodos, pero es mucho más fácil adivinar si conocemos la naturaleza de las clases mismas. Bueno, algunos materiales adicionales sobre el tema: ExecutorsubmitFuture #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION