JavaRush /Blog Java /Random-FR /Vous ne pouvez pas ruiner Java avec un fil de discussion ...
Viacheslav
Niveau 3

Vous ne pouvez pas ruiner Java avec un fil de discussion : Partie IV - Callable, Future et ses amis

Publié dans le groupe Random-FR

Introduction

Nous avons déjà vu comment les threads sont créés dans la première partie . Souvenons-nous encore. Vous ne pouvez pas gâcher Java avec un fil de discussion : Partie IV - Callable, Future and Friends - 1Un thread est Threadquelque chose qui s'y exécute run, utilisons donc le compilateur en ligne Java tutorielspoint et exécutons le code suivant :

public class HelloWorld {
    
    public static void main(String []args){
        Runnable task = () -> {
            System.out.println("Hello World");
        };
        new Thread(task).start();
    }
}
Est-ce la seule option pour exécuter une tâche dans un thread ?

java.util.concurrent.Callable

Il s'avère que java.lang.Runnable a un frère et son nom est java.util.concurrent.Callable et il est né en Java 1.5. Quelles sont les différences? Si l'on regarde de plus près le JavaDoc de cette interface, on constate que, contrairement à Runnable, la nouvelle interface déclare une méthode callqui renvoie un résultat. De plus, par défaut, il lève une exception. Autrement dit, cela nous évite d'avoir à écrire try-catchdes blocs pour les exceptions vérifiées. Pas mal déjà, non ? Nous avons maintenant Runnableune nouvelle tâche à la place :

Callable task = () -> {
	return "Hello, World!";
};
Mais qu’en faire ? Pourquoi avons-nous même besoin d’une tâche exécutée sur un thread qui renvoie un résultat ? Évidemment, à l’avenir, nous nous attendons à recevoir le résultat des actions qui seront réalisées dans le futur. Avenir en anglais - Avenir. Et il existe une interface portant exactement le même nom :java.util.concurrent.Future

java.util.concurrent.Future

L' interface java.util.concurrent.Future décrit une API pour travailler avec des tâches dont nous prévoyons d'obtenir les résultats à l'avenir : méthodes d'obtention des résultats, méthodes de vérification de l'état. Nous Futuresommes intéressés par son implémentation java.util.concurrent.FutureTask . Autrement dit Task, c'est ce qui sera exécuté dans Future. Ce qui est également intéressant dans cette implémentation, c'est qu'elle implémente et Runnable. Vous pouvez considérer cela comme une sorte d'adaptateur de l'ancien modèle de travail avec des tâches dans les threads et du nouveau modèle (nouveau dans le sens où il est apparu dans Java 1.5). Voici un exemple :

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());
    }
}
Comme le montre l'exemple, en utilisant la méthode, nous obtenons getle résultat du problème task. (!)Important, qu'au moment où le résultat est obtenu à l'aide de la méthode, getl'exécution devient synchrone. Selon vous, quel mécanisme sera utilisé ici ? C'est vrai, il n'y a pas de bloc de synchronisation - nous verrons donc WAITING dans JVisualVM non pas comme monitorou wait, mais comme le même park(puisque le mécanisme est utilisé LockSupport).

Interfaces fonctionnelles

Nous parlerons ensuite des classes de Java 1.8, il serait donc utile de faire une brève introduction. Regardons le code suivant :

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);
	}
};
Il y a beaucoup de code inutile, n'est-ce pas ? Chacune des classes déclarées remplit une seule fonction, mais pour la décrire, nous utilisons un tas de code auxiliaire inutile. Et les développeurs Java le pensaient aussi. Par conséquent, ils ont introduit un ensemble d'« interfaces fonctionnelles » ( @FunctionalInterface) et ont décidé que désormais Java lui-même « réfléchirait » à tout pour nous, à l'exception des plus importants :

Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Supplier- fournisseur. Il n'a pas de paramètres, mais il renvoie quelque chose, c'est-à-dire qu'il le fournit. Consumer- consommateur. Il prend quelque chose en entrée (paramètres) et en fait quelque chose, c'est-à-dire qu'il consomme quelque chose. Il y a une autre fonction. Il prend quelque chose en entrée (paramètre s), fait quelque chose et renvoie quelque chose. Comme nous le voyons, les génériques sont activement utilisés. Si vous n'êtes pas sûr, vous pouvez vous en souvenir et lire " La théorie des génériques en Java ou comment mettre les parenthèses en pratique ."

ComplétableFutur

Au fil du temps, Java 1.8 a introduit une nouvelle classe appelée CompletableFuture. Il implémente l'interface Future, ce qui signifie que la nôtre tasksera exécutée dans le futur et que nous pourrons l'exécuter getet obtenir le résultat. Mais il en met également en œuvre CompletionStage. D'après la traduction, son objectif est déjà clair : il s'agit d'une certaine étape d'une sorte de calcul. Une brève introduction au sujet peut être trouvée dans l'aperçu " Introduction à CompletionStage et CompletableFuture ". Allons droit au but. Examinons la liste des méthodes statiques disponibles pour nous aider à démarrer : Vous ne pouvez pas gâcher Java avec un fil de discussion : Partie IV - Callable, Future and Friends - 2Voici les options pour les utiliser :

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 "Значение";
        });
    }
}
Si nous exécutons ce code, nous verrons que la création CompletableFutureimplique le démarrage de toute la chaîne. Par conséquent, bien qu’il existe une certaine similitude avec SteamAPI de Java8, c’est la différence entre ces approches. Par exemple:

List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
	System.out.println("Executed");
	return value.toUpperCase();
});
Ceci est un exemple de l'API Java 8 Stream (vous pouvez en savoir plus ici " Guide de l'API Java 8 Stream en images et exemples "). Si vous exécutez ce code, il Executedne s'affichera pas. Autrement dit, lors de la création d'un flux en Java, le flux ne démarre pas immédiatement, mais attend qu'une valeur soit nécessaire. Mais CompletableFutureil démarre immédiatement la chaîne d'exécution, sans attendre qu'on lui demande la valeur calculée. Je pense qu'il est important de comprendre cela. Nous avons donc CompletableFuture. Comment créer une chaîne et de quels moyens disposons-nous ? Rappelons-nous les interfaces fonctionnelles dont nous avons parlé plus tôt.
  • Nous avons une fonction ( Function) qui prend A et renvoie B. Elle a une seule méthode - apply(appliquer).
  • Nous avons un consommateur ( Consumer) qui accepte A et ne renvoie rien ( Void ). Il n'a qu'une seule méthode : accept(accepter).
  • Nous avons du code exécuté sur un thread Runnablequi n'accepte ni ne renvoie. Il a une seule méthode - run(exécuter).
La deuxième chose à retenir est que CompletalbeFuturedans son travail, il utilise Runnabledes consommateurs et des fonctions. Compte tenu de cela, vous pouvez toujours vous rappeler que vous CompletableFuturepouvez faire ceci :

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);
}
Les méthodes thenRunont thenApplydes versions . thenAccept_ AsyncCela signifie que ces étapes seront exécutées dans un nouveau thread. Il sera prélevé dans un bassin spécial, on ne sait donc pas à l'avance de quel type de flux il s'agira, nouveau ou ancien. Tout dépend de la difficulté des tâches. En plus de ces méthodes, il existe trois autres possibilités intéressantes. Pour plus de clarté, imaginons que nous ayons un certain service qui reçoit un message de quelque part et que cela prend du temps :

public static class NewsService {
	public static String getMessage() {
		try {
			Thread.currentThread().sleep(3000);
			return "Message";
		} catch (InterruptedException e) {
			throw new IllegalStateException(e);
		}
	}
}
Voyons maintenant les autres fonctionnalités que CompletableFuture. On peut combiner le résultat CompletableFutureavec le résultat d'un autre 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();
Il convient de noter que par défaut, les threads seront des threads démons, donc pour plus de clarté get, nous avons l'habitude d'attendre le résultat. Et on peut non seulement combiner (combiner), mais aussi retourner CompletableFuture:

CompletableFuture.completedFuture(2L)
				.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
                               .thenAccept(result -> System.out.println(result));
Ici, je voudrais noter que par souci de concision, la méthode a été utilisée CompletableFuture.completedFuture. Cette méthode ne crée pas de nouveau thread, donc le reste de la chaîne sera exécuté dans le même thread dans lequel elle a été appelée completedFuture. Il existe également une méthode thenAcceptBoth. C'est très similaire à accept, mais s'il thenAcceptaccepte consumer, alors thenAcceptBothil accepte un autre CompletableStage+ comme entrée BiConsumer, c'est-à-dire consumerqui accepte 2 sources comme entrée, pas une. Il y a une autre possibilité intéressante avec le mot Either: Vous ne pouvez pas ruiner Java avec un fil : Partie IV - Callable, Future et ses amis - 3Ces méthodes acceptent une alternative CompletableStageet seront exécutées sur celle CompletableStagequi est exécutée en premier. Et j'aimerais terminer cette revue avec une autre fonctionnalité intéressante CompletableFuture: la gestion des erreurs.

CompletableFuture.completedFuture(2L)
				 .thenApply((a) -> {
					throw new IllegalStateException("error");
				 }).thenApply((a) -> 3L)
				 //.exceptionally(ex -> 0L)
				 .thenAccept(val -> System.out.println(val));
Ce code ne fera rien, car... une exception sera levée et rien ne se passera. Mais si nous décommentons exceptionally, alors nous définissons le comportement. CompletableFutureJe recommande également de regarder la vidéo suivante sur ce sujet : À mon humble avis, ces vidéos sont parmi les plus visuelles d’Internet. Ils devraient comprendre clairement comment tout cela fonctionne, de quel arsenal nous disposons et pourquoi tout cela est nécessaire.

Conclusion

Espérons que la manière dont les threads peuvent être utilisés pour récupérer des calculs après leur calcul soit désormais claire. Matériels supplémentaires: #Viacheslav
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION