JavaRush /Blog Java /Random-ES /No puedes arruinar Java con un hilo: Parte IV - Invocable...
Viacheslav
Nivel 3

No puedes arruinar Java con un hilo: Parte IV - Invocable, Futuro y amigos

Publicado en el grupo Random-ES

Introducción

Ya hemos visto cómo se crean los hilos en la primera parte . Recordemos de nuevo. No puedes estropear Java con un hilo: Parte IV - Invocable, Futuro y amigos - 1Un hilo es Threadalgo que se ejecuta en él run, así que usemos el compilador en línea de Java de tutorialspoint y ejecutemos el siguiente código:

public class HelloWorld {
    
    public static void main(String []args){
        Runnable task = () -> {
            System.out.println("Hello World");
        };
        new Thread(task).start();
    }
}
¿Es esta la única opción para ejecutar una tarea en un hilo?

java.util.concurrent.Invocable

Resulta que java.lang.Runnable tiene un hermano y su nombre es java.util.concurrent.Callable y nació en Java 1.5. ¿Cuáles son las diferencias? Si miramos más de cerca el JavaDoc de esta interfaz, vemos que, a diferencia de Runnable, la nueva interfaz declara un método callque devuelve un resultado. Además, de forma predeterminada arroja una excepción. Es decir, nos ahorra la necesidad de escribir try-catchbloques para excepciones marcadas. Ya no está mal, ¿verdad? Ahora tenemos Runnableuna nueva tarea en su lugar:

Callable task = () -> {
	return "Hello, World!";
};
Pero qué hacer con eso? ¿Por qué necesitamos una tarea ejecutándose en un hilo que devuelve un resultado? Evidentemente, en el futuro esperamos recibir el resultado de las acciones que se realizarán en el futuro. Futuro en inglés - Futuro. Y hay una interfaz con exactamente el mismo nombre:java.util.concurrent.Future

java.util.concurrent.Futuro

La interfaz java.util.concurrent.Future describe una API para trabajar con tareas cuyos resultados planeamos obtener en el futuro: métodos para obtener resultados, métodos para verificar el estado. Nos Futureinteresa su implementación java.util.concurrent.FutureTask . Es decir Task, esto es lo que se ejecutará en Future. Lo que también es interesante acerca de esta implementación es que implementa y Runnable. Puede considerar esto como una especie de adaptador del modelo antiguo de trabajar con tareas en subprocesos y el nuevo modelo (nuevo en el sentido de que apareció en Java 1.5). He aquí un ejemplo:

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class HelloWorld {
    
    public static void main(String []args) throws Exception {
        Callable task = () -> {
            return "Hello, World!";
        };
        FutureTask<String> future = new FutureTask<>(task);
        new Thread(future).start();
        System.out.println(future.get());
    }
}
Como se puede ver en el ejemplo, usando el método obtenemos getel resultado del problema task. (!)Importante, que en el momento en que se obtiene el resultado utilizando el método, getla ejecución se vuelve sincrónica. ¿Qué mecanismo crees que se utilizará aquí? Así es, no hay ningún bloque de sincronización; por lo tanto, veremos ESPERA en JVisualVM no como monitoro wait, sino como el mismo park(ya que se utiliza el mecanismo LockSupport).

Interfaces funcionales

A continuación hablaremos de las clases de Java 1.8, por lo que sería útil hacer una breve introducción. Veamos el siguiente código:

Supplier<String> supplier = new Supplier<String>() {
	@Override
	public String get() {
		return "String";
	}
};
Consumer<String> consumer = new Consumer<String>() {
	@Override
	public void accept(String s) {
		System.out.println(s);
	}
};
Function<String, Integer> converter = new Function<String, Integer>() {
	@Override
	public Integer apply(String s) {
		return Integer.valueOf(s);
	}
};
Hay mucho código innecesario, ¿no? Cada una de las clases declaradas realiza una única función, pero para describirla utilizamos un montón de código auxiliar innecesario. Y los desarrolladores de Java también lo pensaron. Por lo tanto, introdujeron un conjunto de "interfaces funcionales" ( @FunctionalInterface) y decidieron que ahora el propio Java "pensará" todo por nosotros, excepto lo importante:

Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Supplier- proveedor. No tiene parámetros, pero devuelve algo, es decir, lo suministra. Consumer- consumidor. Toma algo como entrada (parámetro s) y hace algo con ello, es decir, consume algo. Hay otra función. Toma algo como entrada (parámetro s), hace algo y devuelve algo. Como vemos, los genéricos se utilizan activamente. Si no estás seguro, puedes recordarlos y leer “ La teoría de los genéricos en Java o cómo poner paréntesis en la práctica ”.

Futuro Completable

Con el paso del tiempo, Java 1.8 introdujo una nueva clase llamada CompletableFuture. Implementa la interfaz Future, lo que significa que la nuestra taskse ejecutará en el futuro y podremos ejecutarla gety obtener el resultado. Pero también implementa algunas CompletionStage. De la traducción ya queda claro su propósito: es una etapa determinada de algún tipo de cálculo. Puede encontrar una breve introducción al tema en la descripción general " Introducción a CompletionStage y CompletableFuture ". Vayamos directo al grano. Veamos la lista de métodos estáticos disponibles para ayudarnos a comenzar: No puedes estropear Java con un hilo: Parte IV - Invocable, Futuro y amigos - 2Aquí están las opciones para usarlos:

import java.util.concurrent.CompletableFuture;
public class App {
    public static void main(String []args) throws Exception {
        // CompletableFuture уже содержащий результат
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Просто significado");
        // CompletableFuture, запускающий (run) новый поток с Runnable, поэтому он Void
        CompletableFuture<Void> voidCompletableFuture;
        voidCompletableFuture = CompletableFuture.runAsync(() -> {
            System.out.println("run " + Thread.currentThread().getName());
        });
        // CompletableFuture, запускающий новый поток, результат которого возьмём у Supplier 
        CompletableFuture<String> supplier;
        supplier = CompletableFuture.supplyAsync(() -> {
            System.out.println("supply " + Thread.currentThread().getName());
            return "Значение";
        });
    }
}
Si ejecutamos este código, veremos que la creación CompletableFutureimplica iniciar toda la cadena. Por lo tanto, si bien existe cierta similitud con SteamAPI de Java8, esta es la diferencia entre estos enfoques. Por ejemplo:

List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
	System.out.println("Executed");
	return value.toUpperCase();
});
Este es un ejemplo de Java 8 Stream Api (puede leer más sobre esto aquí " Guía de Java 8 Stream API en imágenes y ejemplos "). Si ejecuta este código, Executedno se mostrará. Es decir, al crear una secuencia en Java, la secuencia no se inicia inmediatamente, sino que espera hasta que se necesita un valor. Pero CompletableFutureinicia la cadena de ejecución inmediatamente, sin esperar a que se le solicite el valor calculado. Creo que es importante entender esto. Entonces tenemos CompletableFuture. ¿Cómo podemos crear una cadena y de qué medios disponemos? Recordemos las interfaces funcionales sobre las que escribimos anteriormente.
  • Tenemos una función ( Function) que toma A y devuelve B. Tiene un único método: apply(aplicar).
  • Tenemos un consumidor ( Consumer) que acepta A y no devuelve nada ( Void ). Tiene un solo método: accept(aceptar).
  • Tenemos un código ejecutándose en un hilo Runnableque no acepta ni regresa. Tiene un único método: run(ejecutar).
Lo segundo que hay que recordar es que CompletalbeFutureen su trabajo utiliza Runnableconsumidores y funciones. Ante esto, siempre puedes recordar que puedes CompletableFuturehacer esto:

public static void main(String []args) throws Exception {
        AtomicLong longValue = new AtomicLong(0);
        Runnable task = () -> longValue.set(new Date().getTime());
        Function<Long, Date> dateConverter = (longvalue) -> new Date(longvalue);
        Consumer<Date> printer = date -> {
            System.out.println(date);
            System.out.flush();
        };
        // CompletableFuture computation
        CompletableFuture.runAsync(task)
                         .thenApply((v) -> longValue.get())
                         .thenApply(dateConverter)
                         .thenAccept(printer);
}
Los métodos thenRuntienen thenApplyversiones._ thenAccept_ AsyncEsto significa que estas etapas se ejecutarán en un nuevo hilo. Se tomará de una piscina especial, por lo que no se sabe de antemano qué tipo de flujo será, nuevo o viejo. Todo depende de lo difíciles que sean las tareas. Además de estos métodos, existen tres posibilidades más interesantes. Para mayor claridad, imaginemos que tenemos un determinado servicio que recibe un mensaje de algún lugar y lleva tiempo:

public static class NewsService {
	public static String getMessage() {
		try {
			Thread.currentThread().sleep(3000);
			return "Message";
		} catch (InterruptedException e) {
			throw new IllegalStateException(e);
		}
	}
}
Ahora, veamos las otras características que CompletableFuture. Podemos combinar el resultado CompletableFuturecon el resultado de otro CompletableFuture:

Supplier newsSupplier = () -> NewsService.getMessage();
        
CompletableFuture<String> reader = CompletableFuture.supplyAsync(newsSupplier);
CompletableFuture.completedFuture("!!")
				 .thenCombine(reader, (a, b) -> b + a)
				 .thenAccept(result -> System.out.println(result))
				 .get();
Vale la pena señalar que, de forma predeterminada, los subprocesos serán subprocesos de demonio, por lo que, para mayor claridad get, solemos esperar el resultado. Y no solo podemos combinar (combinar), sino también devolver CompletableFuture:

CompletableFuture.completedFuture(2L)
				.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
                               .thenAccept(result -> System.out.println(result));
Aquí me gustaría señalar que, por brevedad, se utilizó el método CompletableFuture.completedFuture. Este método no crea un nuevo hilo, por lo que el resto de la cadena se ejecutará en el mismo hilo en el que fue llamado completedFuture. También hay un método thenAcceptBoth. Es muy similar a accept, pero si thenAcceptacepta consumer, entonces thenAcceptBothacepta otro CompletableStage+ como entrada BiConsumer, es decir consumer, que acepta 2 fuentes como entrada, no una. Hay otra posibilidad interesante con la palabra Either: No puedes arruinar Java con un hilo: Parte IV - Invocable, Futuro y amigos - 3Estos métodos aceptan una alternativa CompletableStagey se ejecutarán en el CompletableStageque se ejecute primero. Y me gustaría terminar esta reseña con otra característica interesante CompletableFuture: el manejo de errores.

CompletableFuture.completedFuture(2L)
				 .thenApply((a) -> {
					throw new IllegalStateException("error");
				 }).thenApply((a) -> 3L)
				 //.exceptionally(ex -> 0L)
				 .thenAccept(val -> System.out.println(val));
Este código no hará nada, porque... Se lanzará una excepción y no pasará nada. Pero si descomentamos exceptionally, entonces definimos el comportamiento. CompletableFutureTambién recomiendo ver el siguiente vídeo sobre este tema : En mi humilde opinión, estos vídeos son unos de los más visuales de Internet. Deben dejarles claro cómo funciona todo, qué arsenal tenemos y por qué es necesario.

Conclusión

Con suerte, ahora está claro cómo se pueden utilizar los subprocesos para recuperar cálculos una vez calculados. Material adicional: #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION