CodeGym /Java Blog /Random-IT /Meglio insieme: Java e la classe Thread. Parte IV - Calla...
John Squirrels
Livello 41
San Francisco

Meglio insieme: Java e la classe Thread. Parte IV - Callable, Future e amici

Pubblicato nel gruppo Random-IT

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 - 1Un 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-catchbloccare 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 getmetodo 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 monitoro wait, ma come park()metodo familiare (poiché il LockSupportmeccanismo 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 Supplierfornitura. Non ha parametri, ma restituisce qualcosa. È così che fornisce le cose. A Consumerconsuma. 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' Futureinterfaccia, cioè i nostri compiti saranno completati in futuro e possiamo chiamare get()per ottenere il risultato. Ma implementa anche l' CompletionStageinterfaccia. 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 - 2Ecco 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 CompletableFuturecomporta 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 CompletableFutureavvia 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 Functionche prende una A e restituisce una B. Ha un solo metodo: apply().
  • Abbiamo a Consumerche 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 CompletableFutureusa Runnable, Consumerse Functionsnel 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 CompletableFuturefornisce. Possiamo combinare il risultato di a 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();
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+ BiConsumercome input, cioè a consumerche 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 - 3questi metodi accettano un'alternativa CompletableStagee vengono eseguiti su quello CompletableStageche 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!
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION