introduzione
Abbiamo già visto
nella prima parte come si creano le discussioni . Ricordiamolo ancora.
Un thread è
Thread
qualcosa che viene eseguito al suo interno
run
, quindi utilizziamo
il compilatore online Java 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 eseguire un'attività in un thread?
java.util.concurrent.Callable
Si scopre che
java.lang.Runnable ha un fratello e il suo nome è
java.util.concurrent.Callable ed è nato in Java 1.5. Quali sono le differenze? Se diamo uno sguardo più attento al JavaDoc di questa interfaccia, vediamo che, a differenza di
Runnable
, la nuova interfaccia dichiara un metodo
call
che restituisce un risultato. Inoltre, per impostazione predefinita genera un'eccezione. Cioè, ci evita la necessità di scrivere
try-catch
blocchi per le eccezioni verificate. Già non male, vero? Ora abbiamo
Runnable
invece un nuovo compito:
Callable task = () -> {
return "Hello, World!";
};
Ma cosa farne? Perché abbiamo bisogno di un'attività in esecuzione su un thread che restituisca un risultato? Ovviamente in futuro ci aspettiamo di ricevere il risultato delle azioni che verranno eseguite in futuro. Futuro in inglese - Futuro. E c'è un'interfaccia con esattamente lo stesso nome:
java.util.concurrent.Future
java.util.concurrent.Future
L' interfaccia
java.util.concurrent.Future descrive un'API per lavorare con attività i cui risultati prevediamo di ottenere in futuro: metodi per ottenere risultati, metodi per verificare lo stato. Siamo
Future
interessati alla sua implementazione
java.util.concurrent.FutureTask . Cioè
Task
, questo è ciò che verrà eseguito in
Future
. Ciò che è interessante anche in questa implementazione è che implementa e
Runnable
. Puoi considerarlo una sorta di adattatore del vecchio modello di lavoro con le attività nei thread e del nuovo modello (nuovo nel senso che appariva 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 si può vedere dall'esempio, utilizzando il metodo otteniamo
get
il risultato del problema
task
.
(!)Importante, che nel momento in cui si ottiene il risultato utilizzando il metodo,
get
l'esecuzione diventa sincrona. Quale meccanismo pensi che verrà utilizzato qui? Esatto, non esiste un blocco di sincronizzazione, quindi vedremo
WAITING in JVisualVM non come
monitor
o
wait
, ma come lo stesso
park
(poiché viene utilizzato il meccanismo
LockSupport
).
Interfacce funzionali
Successivamente parleremo delle classi da Java 1.8, quindi sarebbe utile fare una breve introduzione. Diamo un'occhiata al 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);
}
};
C'è un sacco di codice non necessario, vero? Ciascuna delle classi dichiarate esegue una singola funzione, ma per descriverla utilizziamo un mucchio di codice ausiliario non necessario. E lo pensavano anche gli sviluppatori Java. Pertanto, hanno introdotto una serie di "interfacce funzionali" (
@FunctionalInterface
) e hanno deciso che ora Java stesso "penserà" a tutto per noi, tranne a quelle importanti:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Supplier
- fornitore. Non ha parametri, ma restituisce qualcosa, cioè lo fornisce.
Consumer
- consumatore. Prende qualcosa come input (parametro s) e fa qualcosa con esso, cioè consuma qualcosa. C'è un'altra funzione. Prende qualcosa come input (parametro
s
), fa qualcosa e restituisce qualcosa. Come vediamo, i farmaci generici vengono utilizzati attivamente. Se non sei sicuro, puoi ricordarli e leggere “
La teoria dei generici in Java o come mettere in pratica le parentesi ”.
CompletabileFuturo
Col passare del tempo, Java 1.8 ha introdotto una nuova classe chiamata
CompletableFuture
. Implementa l'interfaccia
Future
, il che significa che la nostra
task
verrà eseguita in futuro e potremo eseguirla
get
e ottenere il risultato. Ma ne implementa anche alcuni
CompletionStage
. Dalla traduzione il suo scopo è già chiaro: si tratta di una certa fase di una sorta di calcolo. Una breve introduzione all'argomento si trova nella panoramica "
Introduzione a CompletionStage e CompletableFuture ". Andiamo dritti al punto. Diamo un'occhiata all'elenco dei metodi statici disponibili per aiutarci a iniziare:
ecco le opzioni per usarli:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String []args) throws Exception {
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Просто meaning");
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 "Значение";
});
}
}
Se eseguiamo questo codice, vedremo che la creazione
CompletableFuture
implica l'avvio dell'intera catena. Pertanto, sebbene esista una certa somiglianza con SteamAPI di Java8, questa è 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 Java 8 Stream (puoi leggere ulteriori informazioni qui "
Guida all'API Java 8 Stream in immagini ed esempi "). Se esegui questo codice,
Executed
non verrà visualizzato. Cioè, quando si crea uno stream in Java, lo stream non si avvia immediatamente, ma attende finché non viene richiesto un valore. Ma
CompletableFuture
avvia immediatamente la catena di esecuzione, senza attendere che gli venga richiesto il valore calcolato. Penso che sia importante capire questo. Quindi abbiamo CompletableFuture. Come possiamo creare una catena e quali mezzi abbiamo? Ricordiamo le interfacce funzionali di cui abbiamo scritto prima.
- Abbiamo una funzione (
Function
) che accetta A e restituisce B. Ha un unico metodo: apply
(applica).
- Abbiamo un consumatore (
Consumer
) che accetta A e non restituisce nulla ( Void ). Ha un solo metodo: accept
(accetta).
- Abbiamo del codice in esecuzione su un thread
Runnable
che non accetta o restituisce. Ha un unico metodo: run
(esegui).
La seconda cosa da ricordare è che
CompletalbeFuture
nel suo lavoro utilizza
Runnable
consumatori e funzioni. Detto questo, puoi sempre ricordare che puoi
CompletableFuture
fare questo:
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);
}
I metodi
thenRun
hanno
thenApply
versioni .
thenAccept
_
Async
Ciò significa che queste fasi verranno eseguite in un nuovo thread. Verrà prelevato da una vasca speciale, quindi non si sa in anticipo che tipo di flusso sarà, nuovo o vecchio. Tutto dipende da quanto sono difficili i compiti. Oltre a questi metodi, ci sono altre tre possibilità interessanti. Per chiarezza, immaginiamo di avere un determinato servizio che riceve un messaggio da qualche parte e impiega 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 alle altre funzionalità che
CompletableFuture
. Possiamo combinare il risultato
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();
Vale la pena notare che per impostazione predefinita i thread saranno thread daemon, quindi per chiarezza
get
, attendiamo il risultato. E non solo possiamo combinare (combinare), ma anche restituire
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
Qui vorrei sottolineare che per brevità è stato utilizzato il metodo
CompletableFuture.completedFuture
. Questo metodo non crea un nuovo thread, quindi il resto della catena verrà eseguito nello stesso thread in cui è stato chiamato
completedFuture
. Esiste anche un metodo
thenAcceptBoth
. È molto simile a
accept
, ma se
thenAccept
accetta
consumer
, allora
thenAcceptBoth
accetta un altro
CompletableStage
+ come input
BiConsumer
, cioè
consumer
, che accetta 2 fonti come input, non una. C'è un'altra possibilità interessante con la parola
Either
:
questi metodi accettano un'alternativa
CompletableStage
e verranno eseguiti su quello
CompletableStage
eseguito per primo. E vorrei concludere questa recensione con un'altra caratteristica interessante
CompletableFuture
: la gestione degli errori.
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
.thenAccept(val -> System.out.println(val));
Questo codice non farà nulla, perché... verrà generata un'eccezione e non accadrà nulla. Ma se rimuoviamo il commento
exceptionally
, allora definiamo il comportamento.
CompletableFuture
Consiglio inoltre di guardare il seguente video sull'argomento :
A mio modesto parere, questi video sono tra i più visivi su Internet. Dovrebbe essere chiaro da loro come funziona il tutto, quale arsenale abbiamo e perché è tutto necessario.
Conclusione
Si spera che ora sia chiaro come i thread possano essere utilizzati per recuperare i calcoli dopo che sono stati calcolati. Materiale aggiuntivo:
#Viacheslav
GO TO FULL VERSION