JavaRush /Java Blog /Random-IT /Non puoi rovinare Java con un thread: Parte IV - Callable...
Viacheslav
Livello 3

Non puoi rovinare Java con un thread: Parte IV - Callable, Future and friends

Pubblicato nel gruppo Random-IT

introduzione

Abbiamo già visto nella prima parte come si creano le discussioni . Ricordiamolo ancora. Non puoi rovinare Java con un thread: Parte IV - Callable, Future and friends - 1Un thread è Threadqualcosa 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 callche restituisce un risultato. Inoltre, per impostazione predefinita genera un'eccezione. Cioè, ci evita la necessità di scrivere try-catchblocchi per le eccezioni verificate. Già non male, vero? Ora abbiamo Runnableinvece 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 Futureinteressati 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 getil risultato del problema task. (!)Importante, che nel momento in cui si ottiene il risultato utilizzando il metodo, getl'esecuzione diventa sincrona. Quale meccanismo pensi che verrà utilizzato qui? Esatto, non esiste un blocco di sincronizzazione, quindi vedremo WAITING in JVisualVM non come monitoro 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 taskverrà eseguita in futuro e potremo eseguirla gete 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: Non puoi rovinare Java con un thread: Parte IV - Callable, Future and friends - 2ecco le opzioni per usarli:
import java.util.concurrent.CompletableFuture;
public class App {
    public static void main(String []args) throws Exception {
        // CompletableFuture уже содержащий результат
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Просто meaning");
        // 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 "Значение";
        });
    }
}
Se eseguiamo questo codice, vedremo che la creazione CompletableFutureimplica 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, Executednon 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 CompletableFutureavvia 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 Runnableche non accetta o restituisce. Ha un unico metodo: run(esegui).
La seconda cosa da ricordare è che CompletalbeFuturenel suo lavoro utilizza Runnableconsumatori e funzioni. Detto questo, puoi sempre ricordare che puoi CompletableFuturefare 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 computation
        CompletableFuture.runAsync(task)
                         .thenApply((v) -> longValue.get())
                         .thenApply(dateConverter)
                         .thenAccept(printer);
}
I metodi thenRunhanno thenApplyversioni . thenAccept_ AsyncCiò 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 CompletableFuturecon 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 thenAcceptaccetta consumer, allora thenAcceptBothaccetta 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: Non puoi rovinare Java con un thread: Parte IV - Callable, Future and friends - 3questi metodi accettano un'alternativa CompletableStagee verranno eseguiti su quello CompletableStageeseguito 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)
				 //.exceptionally(ex -> 0L)
				 .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. CompletableFutureConsiglio 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
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION