Introduction
Nous avons déjà vu comment les threads sont créés dans
la première partie . Souvenons-nous encore.
Un thread est
Thread
quelque 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
call
qui renvoie un résultat. De plus, par défaut, il lève une exception. Autrement dit, cela nous évite d'avoir à écrire
try-catch
des blocs pour les exceptions vérifiées. Pas mal déjà, non ? Nous avons maintenant
Runnable
une 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
Future
sommes 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
get
le résultat du problème
task
.
(!)Important, qu'au moment où le résultat est obtenu à l'aide de la méthode,
get
l'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
monitor
ou
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
task
sera exécutée dans le futur et que nous pourrons l'exécuter
get
et 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 :
Voici les options pour les utiliser :
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 "Значение";
});
}
}
Si nous exécutons ce code, nous verrons que la création
CompletableFuture
implique 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
Executed
ne 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
CompletableFuture
il 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
Runnable
qui n'accepte ni ne renvoie. Il a une seule méthode - run
(exécuter).
La deuxième chose à retenir est que
CompletalbeFuture
dans son travail, il utilise
Runnable
des consommateurs et des fonctions. Compte tenu de cela, vous pouvez toujours vous rappeler que vous
CompletableFuture
pouvez 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.runAsync(task)
.thenApply((v) -> longValue.get())
.thenApply(dateConverter)
.thenAccept(printer);
}
Les méthodes
thenRun
ont
thenApply
des versions .
thenAccept
_
Async
Cela 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
CompletableFuture
avec 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
thenAccept
accepte
consumer
, alors
thenAcceptBoth
il accepte un autre
CompletableStage
+ comme entrée
BiConsumer
, c'est-à-dire
consumer
qui accepte 2 sources comme entrée, pas une. Il y a une autre possibilité intéressante avec le mot
Either
:
Ces méthodes acceptent une alternative
CompletableStage
et seront exécutées sur celle
CompletableStage
qui 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)
.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.
CompletableFuture
Je 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
GO TO FULL VERSION