CodeGym /Blog Java /Random-FR /Mieux ensemble : Java et la classe Thread. Partie IV - Ca...
John Squirrels
Niveau 41
San Francisco

Mieux ensemble : Java et la classe Thread. Partie IV - Callable, Future et amis

Publié dans le groupe Random-FR

Introduction

Dans la partie I , nous avons examiné comment les threads sont créés. Rappelons-nous encore une fois. Mieux ensemble : Java et la classe Thread.  Partie IV - Callable, Future et amis - 1Un thread est représenté par la classe Thread, dont run()la méthode est appelée. Utilisons donc le compilateur Java en ligne Tutorialspoint 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 démarrer une tâche sur un thread ?

java.util.concurrent.Callable

Il s'avère que java.lang.Runnable a un frère appelé java.util.concurrent.Callable qui est venu au monde en Java 1.5. Quelles sont les différences? Si vous regardez attentivement le Javadoc de cette interface, nous voyons que, contrairement à Runnable, la nouvelle interface déclare une call()méthode qui renvoie un résultat. En outre, il lève une exception par défaut. Autrement dit, cela nous évite d'avoir à try-catchbloquer les exceptions vérifiées. Pas mal, non ? Maintenant, nous avons une nouvelle tâche au lieu deRunnable :

Callable task = () -> {
	return "Hello, World!";
};
Mais qu'est-ce qu'on en fait ? Pourquoi avons-nous besoin d'une tâche exécutée sur un thread qui renvoie un résultat ? Évidemment, pour toute action effectuée à l'avenir, nous nous attendons à recevoir le résultat de ces actions à l'avenir. Et nous avons une interface avec un nom correspondant :java.util.concurrent.Future

java.util.concurrent.Future

L' interface java.util.concurrent.Future définit une API pour travailler avec des tâches dont nous prévoyons de recevoir les résultats dans le futur : des méthodes pour obtenir un résultat et des méthodes pour vérifier l'état. En ce qui concerne Future, nous nous intéressons à son implémentation dans la classe java.util.concurrent.FutureTask . C'est la "Tâche" qui sera exécutée dans Future. Ce qui rend cette implémentation encore plus intéressante, c'est qu'elle implémente également Runnable. Vous pouvez considérer cela comme une sorte d'adaptateur entre l'ancien modèle de travail avec des tâches sur les threads et le 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 vous pouvez le voir dans l'exemple, nous utilisons la getméthode pour obtenir le résultat de la tâche. Note:lorsque vous obtenez le résultat en utilisant la get()méthode, l'exécution devient synchrone ! Selon vous, quel mécanisme sera utilisé ici ? Certes, il n'y a pas de bloc de synchronisation. C'est pourquoi nous ne verrons pas WAITING dans JVisualVM comme un monitorou wait, mais comme la park()méthode familière (parce que le LockSupportmécanisme est utilisé).

Interfaces fonctionnelles

Ensuite, nous parlerons des classes de Java 1.8, nous ferions donc bien de fournir une brève introduction. Regardez 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);
	}
};
Beaucoup, beaucoup de code supplémentaire, n'est-ce pas ? Chacune des classes déclarées exécute une fonction, mais nous utilisons un tas de code de support supplémentaire pour la définir. Et c'est ainsi que pensaient les développeurs Java. En conséquence, ils ont introduit un ensemble "d'interfaces fonctionnelles" ( @FunctionalInterface) et ont décidé que Java lui-même ferait désormais la "réflexion", ne nous laissant que les choses importantes à nous soucier :

Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Un Supplierravitaillement. Il n'a pas de paramètres, mais il renvoie quelque chose. C'est ainsi qu'il fournit les choses. A Consumerconsomme. Il prend quelque chose comme entrée (un argument) et en fait quelque chose. L'argument est ce qu'il consomme. Ensuite, nous avons également Function. Il prend des entrées (arguments), fait quelque chose et renvoie quelque chose. Vous pouvez voir que nous utilisons activement des génériques. Si vous n'êtes pas sûr, vous pouvez obtenir un rappel en lisant " Les génériques en Java : comment utiliser les crochets en pratique ".

ComplétableFutur

Le temps a passé et une nouvelle classe appelée CompletableFutureest apparue dans Java 1.8. Il implémente l' Futureinterface, c'est-à-dire que nos tâches seront terminées dans le futur, et nous pouvons appeler get()pour obtenir le résultat. Mais il implémente également l' CompletionStageinterface. Le nom dit tout : il s'agit d'une certaine étape d'un ensemble de calculs. Une brève introduction au sujet peut être trouvée dans la revue ici : Introduction à CompletionStage et CompletableFuture. Allons droit au but. Examinons la liste des méthodes statiques disponibles qui nous aideront à démarrer : Mieux ensemble : Java et la classe Thread.  Partie IV - Callable, Future et amis - 2voici les options pour les utiliser :

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";
        });
    }
}
Si nous exécutons ce code, nous verrons que la création d'un CompletableFutureimplique également le lancement d'un pipeline complet. Par conséquent, avec une certaine similitude avec la SteamAPI de Java8, c'est là que l'on trouve 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 Stream de Java 8. Si vous exécutez ce code, vous verrez que "Executed" ne sera pas affiché. En d'autres termes, lorsqu'un flux est créé en Java, le flux ne démarre pas immédiatement. Au lieu de cela, il attend que quelqu'un en veuille une valeur. Mais CompletableFuturecommence à exécuter le pipeline immédiatement, sans attendre que quelqu'un lui demande une valeur. Je pense que c'est important de comprendre. Donc, nous avons un CompletableFuture. Comment pouvons-nous créer un pipeline (ou une chaîne) et quels mécanismes avons-nous ? Rappelez-vous ces interfaces fonctionnelles dont nous avons parlé plus tôt.
  • Nous avons a Functionqui prend un A et renvoie un B. Il a une seule méthode : apply().
  • Nous avons un Consumerqui prend un A et ne renvoie rien (Void). Il a une seule méthode : accept().
  • Nous avons Runnable, qui s'exécute sur le thread, ne prend rien et ne renvoie rien. Il a une seule méthode : run().
La prochaine chose à retenir est qu'il CompletableFutureutilise Runnable, Consumerset Functionsdans son travail. En conséquence, vous pouvez toujours savoir que vous pouvez faire ce qui suit avec 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);
}
Les méthodes thenRun(), thenApply()et thenAccept()ont des versions "Async". Cela signifie que ces étapes seront réalisées sur un thread différent. Ce fil sera tiré d'un pool spécial - nous ne saurons donc pas à l'avance s'il s'agira d'un nouveau ou d'un ancien fil. Tout dépend de l'intensité de calcul 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 une sorte de message de quelque part — et 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);
		}
	}
}
Maintenant, jetons un coup d'œil aux autres capacités CompletableFuturefournies. On peut combiner le résultat d'un 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();
Notez que les threads sont des threads démons par défaut, donc pour plus de clarté, nous utilisons get()pour attendre le résultat. Non seulement pouvons-nous combiner CompletableFutures, nous pouvons également retourner un CompletableFuture:

CompletableFuture.completedFuture(2L)
				.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
                               .thenAccept(result -> System.out.println(result));
Ici, je tiens à souligner que la CompletableFuture.completedFuture()méthode a été utilisée par souci de brièveté. Cette méthode ne crée pas de nouveau thread, donc le reste du pipeline sera exécuté sur le même thread où completedFutureil a été appelé. Il existe aussi une thenAcceptBoth()méthode. C'est très similaire à accept(), mais si thenAccept()accepte a Consumer, thenAcceptBoth()accepte un autre CompletableStage+ BiConsumeren entrée, c'est-à-dire a consumerqui prend 2 sources au lieu d'une. Il y a une autre capacité intéressante offerte par les méthodes dont le nom inclut le mot "Soit" : Mieux ensemble : Java et la classe Thread.  Partie IV - Callable, Future et amis - 3Ces méthodes acceptent une alternative CompletableStageet sont exécutées sur celle CompletableStagequi doit être exécutée en premier. Enfin, je veux terminer cette revue avec une autre fonctionnalité intéressante de 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 il y aura une exception et rien d'autre ne se passera. Mais en décommentant la déclaration "exceptionnellement", nous définissons le comportement attendu. En parlant de CompletableFuture, je vous recommande également de regarder la vidéo suivante : À mon humble avis, ce sont parmi les vidéos les plus explicatives sur Internet. Ils devraient expliquer clairement comment tout cela fonctionne, quelle boîte à outils nous avons à disposition et pourquoi tout cela est nécessaire.

Conclusion

Heureusement, il est maintenant clair comment vous pouvez utiliser les threads pour obtenir des calculs une fois qu'ils sont terminés. Matériels supplémentaires: Mieux ensemble : Java et la classe Thread. Partie I — Threads d'exécution Mieux ensemble : Java et la classe Thread. Partie II — Synchronisation Mieux ensemble : Java et la classe Thread. Partie III — Interaction Mieux ensemble : Java et la classe Thread. Partie V — Executor, ThreadPool, Fork/Join Mieux ensemble : Java et la classe Thread. Partie VI — Tirez !
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION