introduzione
Nella
Parte I , abbiamo esaminato come vengono creati i thread. Ricordiamo ancora una volta.
![Meglio insieme: Java e la classe Thread. Parte IV — Richiamabile, Futuro e amici - 1]()
Un thread è rappresentato dalla classe Thread, il cui
run()
metodo viene chiamato. Quindi usiamo il
compilatore Java online Tutorialspoint ed eseguiamo il seguente codice:
public class HelloWorld {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
È questa l'unica opzione per avviare un'attività su un thread?
java.util.concurrent.Callable
Si scopre che
java.lang.Runnable ha un fratello chiamato
java.util.concurrent.Callable che è venuto al mondo in Java 1.5. Quali sono le differenze? Se osservi attentamente il Javadoc per questa interfaccia, vediamo che, a differenza di
Runnable
, la nuova interfaccia dichiara un
call()
metodo che restituisce un risultato. Inoltre, genera Exception per impostazione predefinita. Cioè, ci evita di dover
try-catch
bloccare le eccezioni verificate. Non male, vero? Ora abbiamo un nuovo compito invece di
Runnable
:
Callable task = () -> {
return "Hello, World!";
};
Ma cosa ne facciamo? Perché abbiamo bisogno di un'attività in esecuzione su un thread che restituisca un risultato? Ovviamente, per qualsiasi azione eseguita in futuro, ci aspettiamo di ricevere il risultato di tali azioni in futuro. E abbiamo un'interfaccia con un nome corrispondente:
java.util.concurrent.Future
java.util.concurrent.Futuro
L' interfaccia
java.util.concurrent.Future definisce un'API per lavorare con attività i cui risultati prevediamo di ricevere in futuro: metodi per ottenere un risultato e metodi per controllare lo stato. Per quanto riguarda
Future
, siamo interessati alla sua implementazione nella classe
java.util.concurrent.FutureTask . Questo è il "Task" che verrà eseguito in
Future
. Ciò che rende questa implementazione ancora più interessante è che implementa anche Runnable. Puoi considerarlo una sorta di adattatore tra il vecchio modello di lavoro con le attività sui thread e il nuovo modello (nuovo nel senso che è apparso in Java 1.5). Ecco un esempio:
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());
}
}
Come puoi vedere dall'esempio, usiamo il
get
metodo per ottenere il risultato dall'attività.
Nota:quando ottieni il risultato usando il
get()
metodo, l'esecuzione diventa sincrona! Quale meccanismo pensi che verrà utilizzato qui? È vero, non esiste un blocco di sincronizzazione. Ecco perché non vedremo
WAITING in JVisualVM come
monitor
o
wait
, ma come
park()
metodo familiare (poiché il
LockSupport
meccanismo viene utilizzato).
Interfacce funzionali
Successivamente, parleremo delle classi di Java 1.8, quindi faremmo bene a fornire una breve introduzione. Guarda il seguente codice:
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);
}
};
Un sacco di codice extra, non diresti? Ognuna delle classi dichiarate esegue una funzione, ma usiamo un sacco di codice di supporto extra per definirla. Ed è così che hanno pensato gli sviluppatori Java. Di conseguenza, hanno introdotto una serie di "interfacce funzionali" (
@FunctionalInterface
) e hanno deciso che ora Java stesso avrebbe "pensato", lasciando a noi solo le cose importanti di cui preoccuparci:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Una
Supplier
fornitura. Non ha parametri, ma restituisce qualcosa. È così che fornisce le cose. A
Consumer
consuma. Prende qualcosa come input (un argomento) e fa qualcosa con esso. L'argomento è ciò che consuma. Poi abbiamo anche
Function
. Prende input (argomenti), fa qualcosa e restituisce qualcosa. Puoi vedere che stiamo utilizzando attivamente i generici. Se non sei sicuro, puoi rinfrescarti leggendo "
Generics in Java: come utilizzare le parentesi angolari nella pratica ".
Futuro Completabile
Il tempo è passato e una nuova classe chiamata
CompletableFuture
è apparsa in Java 1.8. Implementa l'
Future
interfaccia, cioè i nostri compiti saranno completati in futuro e possiamo chiamare
get()
per ottenere il risultato. Ma implementa anche l'
CompletionStage
interfaccia. Il nome dice tutto: questa è una certa fase di una serie di calcoli. Una breve introduzione all'argomento può essere trovata nella recensione qui: Introduzione a CompletionStage e CompletableFuture. Andiamo dritti al punto. Diamo un'occhiata all'elenco dei metodi statici disponibili che ci aiuteranno a iniziare:
![Meglio insieme: Java e la classe Thread. Parte IV — Richiamabile, Futuro e amici - 2]()
Ecco le opzioni per utilizzarli:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String[] args) throws Exception {
// A CompletableFuture that already contains a Result
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Just a value");
// A CompletableFuture that runs a new thread from Runnable. That's why it's Void
CompletableFuture<Void> voidCompletableFuture;
voidCompletableFuture = CompletableFuture.runAsync(() -> {
System.out.println("run " + Thread.currentThread().getName());
});
// A CompletableFuture that starts a new thread whose result we'll get from a Supplier
CompletableFuture<String> supplier;
supplier = CompletableFuture.supplyAsync(() -> {
System.out.println("supply " + Thread.currentThread().getName());
return "Value";
});
}
}
Se eseguiamo questo codice, vedremo che la creazione di a
CompletableFuture
comporta anche l'avvio di un'intera pipeline. Pertanto, con una certa somiglianza con SteamAPI di Java8, è qui che troviamo la differenza tra questi approcci. Per esempio:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
Questo è un esempio dell'API Stream di Java 8. Se esegui questo codice, vedrai che "Eseguito" non verrà visualizzato. In altre parole, quando viene creato un flusso in Java, il flusso non si avvia immediatamente. Invece, aspetta che qualcuno ne voglia un valore. Ma
CompletableFuture
avvia immediatamente l'esecuzione della pipeline, senza attendere che qualcuno chieda un valore. Penso che questo sia importante da capire. Quindi, abbiamo un file
CompletableFuture
. Come possiamo realizzare una pipeline (o catena) e quali meccanismi abbiamo? Ricorda quelle interfacce funzionali di cui abbiamo scritto in precedenza.
- Abbiamo a
Function
che prende una A e restituisce una B. Ha un solo metodo: apply()
.
- Abbiamo a
Consumer
che accetta una A e non restituisce nulla (Vuoto). Ha un unico metodo: accept()
.
- Abbiamo
Runnable
, che viene eseguito sul thread, non prende nulla e non restituisce nulla. Ha un unico metodo: run()
.
La prossima cosa da ricordare è che
CompletableFuture
usa
Runnable
,
Consumers
e
Functions
nel suo lavoro. Di conseguenza, puoi sempre sapere che puoi fare quanto segue con
CompletableFuture
:
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);
}
I metodi
thenRun()
,
thenApply()
e
thenAccept()
hanno versioni "Async". Ciò significa che queste fasi verranno completate su un thread diverso. Questo thread verrà preso da un pool speciale, quindi non sapremo in anticipo se sarà un thread nuovo o vecchio. Tutto dipende dall'intensità computazionale delle attività. Oltre a questi metodi, ci sono altre tre possibilità interessanti. Per chiarezza, immaginiamo di avere un certo servizio che riceve un qualche tipo di messaggio da qualche parte - e questo richiede tempo:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Ora, diamo un'occhiata ad altre abilità che
CompletableFuture
fornisce. Possiamo combinare il risultato di a
CompletableFuture
con il risultato di un altro
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();
Nota che i thread sono thread daemon per impostazione predefinita, quindi per chiarezza, usiamo
get()
per attendere il risultato. Non solo possiamo combinare
CompletableFutures
, ma possiamo anche restituire un
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
Qui voglio notare che il
CompletableFuture.completedFuture()
metodo è stato utilizzato per brevità. Questo metodo non crea un nuovo thread, quindi il resto della pipeline verrà eseguito sullo stesso thread in cui
completedFuture
è stato chiamato. C'è anche un
thenAcceptBoth()
metodo. È molto simile a
accept()
, ma se
thenAccept()
accetta a
Consumer
,
thenAcceptBoth()
accetta un altro
CompletableStage
+
BiConsumer
come input, cioè a
consumer
che prende 2 sorgenti invece di una. C'è un'altra capacità interessante offerta dai metodi il cui nome include la parola "o":
![Meglio insieme: Java e la classe Thread. Parte IV — Richiamabile, Futuro e amici - 3]()
questi metodi accettano un'alternativa
CompletableStage
e vengono eseguiti su quello
CompletableStage
che viene eseguito per primo. Infine, voglio concludere questa recensione con un'altra caratteristica interessante di
CompletableFuture
: la gestione degli errori.
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
//.exceptionally(ex -> 0L)
.thenAccept(val -> System.out.println(val));
Questo codice non farà nulla, perché ci sarà un'eccezione e non accadrà nient'altro. Ma decommentando l'affermazione "eccezionalmente", definiamo il comportamento previsto. A proposito
CompletableFuture
, ti consiglio anche di guardare il seguente video:
A mio modesto parere, questi sono tra i video più esplicativi su Internet. Dovrebbero chiarire come funziona tutto questo, quale toolkit abbiamo a disposizione e perché tutto questo è necessario.
Conclusione
Si spera che ora sia chiaro come utilizzare i thread per ottenere i calcoli dopo che sono stati completati. Materiale aggiuntivo:
Meglio insieme: Java e la classe Thread. Parte I — Thread di esecuzione Meglio insieme: Java e la classe Thread. Parte II — Sincronizzazione Meglio insieme: Java e la classe Thread. Parte III — Interazione Meglio insieme: Java e la classe Thread. Parte V — Executor, ThreadPool, Fork/Join Better insieme: Java e la classe Thread. Parte VI - Spara via!
GO TO FULL VERSION