Introducción
Ya hemos visto cómo se crean los hilos en
la primera parte . Recordemos de nuevo.
Un hilo es
Thread
algo 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
call
que devuelve un resultado. Además, de forma predeterminada arroja una excepción. Es decir, nos ahorra la necesidad de escribir
try-catch
bloques para excepciones marcadas. Ya no está mal, ¿verdad? Ahora tenemos
Runnable
una 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
Future
interesa 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
get
el resultado del problema
task
.
(!)Importante, que en el momento en que se obtiene el resultado utilizando el método,
get
la 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
monitor
o
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
task
se ejecutará en el futuro y podremos ejecutarla
get
y 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:
Aquí están las opciones para usarlos:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String []args) throws Exception {
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Просто significado");
CompletableFuture<Void> voidCompletableFuture;
voidCompletableFuture = CompletableFuture.runAsync(() -> {
System.out.println("run " + Thread.currentThread().getName());
});
CompletableFuture<String> supplier;
supplier = CompletableFuture.supplyAsync(() -> {
System.out.println("supply " + Thread.currentThread().getName());
return "Значение";
});
}
}
Si ejecutamos este código, veremos que la creación
CompletableFuture
implica 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,
Executed
no 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
CompletableFuture
inicia 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
Runnable
que no acepta ni regresa. Tiene un único método: run
(ejecutar).
Lo segundo que hay que recordar es que
CompletalbeFuture
en su trabajo utiliza
Runnable
consumidores y funciones. Ante esto, siempre puedes recordar que puedes
CompletableFuture
hacer 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.runAsync(task)
.thenApply((v) -> longValue.get())
.thenApply(dateConverter)
.thenAccept(printer);
}
Los métodos
thenRun
tienen
thenApply
versiones._
thenAccept
_
Async
Esto 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
CompletableFuture
con 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
thenAccept
acepta
consumer
, entonces
thenAcceptBoth
acepta otro
CompletableStage
+ como entrada
BiConsumer
, es decir
consumer
, que acepta 2 fuentes como entrada, no una. Hay otra posibilidad interesante con la palabra
Either
:
Estos métodos aceptan una alternativa
CompletableStage
y se ejecutarán en el
CompletableStage
que 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)
.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.
CompletableFuture
Tambié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
GO TO FULL VERSION