Introduction
Le multithreading est intégré à Java depuis ses débuts. Jetons donc un rapide coup d'œil à ce qu'est le multithreading.
Prenons comme point de départ la leçon officielle d'Oracle : "
Leçon : L'application "Hello World! ". Modifions un peu le code de notre application Hello World comme suit :
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args
est un tableau de paramètres d'entrée transmis au démarrage du programme. Sauvegardons ce code dans un fichier dont le nom correspond au nom de la classe et à l'extension
.java
. Compilons à l'aide de l' utilitaire
javac :
javac HelloWorldApp.java
Après cela, appelons notre code avec un paramètre, par exemple Roger :
java HelloWorldApp Roger
Notre code a maintenant un sérieux défaut. Si nous ne transmettons aucun argument (c'est-à-dire exécutons simplement Java HelloWorldApp), nous obtiendrons une erreur :
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
Une exception (c'est-à-dire une erreur) s'est produite dans un thread nommé
main
. Il s'avère qu'il existe des sortes de threads en Java ? C'est ici que commence notre voyage.
Java et les threads
Pour comprendre ce qu'est un thread, vous devez comprendre comment une application Java est lancée. Modifions notre code comme suit :
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
}
}
}
Maintenant, compilons-le à nouveau en utilisant javac. Ensuite, pour plus de commodité, nous exécuterons notre code Java dans une fenêtre séparée. Sous Windows, vous pouvez procéder ainsi :
start java HelloWorldApp
. Maintenant, à l'aide de l' utilitaire
jps , voyons quelles informations Java nous dira :
Le premier numéro est le PID ou Process ID, l'identifiant du processus. Qu'est-ce qu'un processus ?
Процесс — это совокупность codeа и данных, разделяющих общее виртуальное addressное пространство.
A l'aide de processus, l'exécution des différents programmes est isolée les unes des autres : chaque application utilise sa propre zone mémoire sans interférer avec les autres programmes. Je vous conseille de lire l'article plus en détail : «
https://habr.com/post/164487/ ». Un processus ne peut pas exister sans threads, donc si un processus existe, au moins un thread existe. Comment cela se produit-il en Java ? Lorsque nous exécutons un programme Java, son exécution commence par le
main
. Nous entrons en quelque sorte dans le programme, donc cette méthode spéciale
main
est appelée le point d'entrée, ou « point d'entrée ». La méthode
main
doit toujours être
public static void
telle que la machine virtuelle Java (JVM) puisse commencer à exécuter notre programme. Voir «
Pourquoi la méthode principale Java est-elle statique ? » pour plus de détails. Il s'avère que le lanceur Java (java.exe ou javaw.exe) est une application simple (simple application C) : il charge diverses DLL, qui sont en réalité la JVM. Le lanceur Java effectue un ensemble spécifique d'appels Java Native Interface (JNI). JNI est le mécanisme qui relie le monde de la machine virtuelle Java et le monde du C++. Il s'avère que le lanceur n'est pas la JVM, mais son chargeur. Il connaît les commandes correctes à exécuter pour démarrer la JVM. Sait organiser tout l'environnement nécessaire à l'aide des appels JNI. Cette organisation de l'environnement comprend également la création d'un fil principal, généralement appelé
main
. Pour voir plus clairement quels threads vivent dans un processus Java, nous utilisons le programme
jvisualvm , qui est inclus dans le JDK. Connaissant le pid d'un processus, nous pouvons ouvrir les données immédiatement :
jvisualvm --openpid айдипроцесса
il est intéressant de noter que chaque thread possède sa propre zone de mémoire distincte allouée au processus. Cette structure de mémoire est appelée pile. Une pile est constituée de frames. Un frame est le point d’appel d’une méthode, point d’exécution. Un cadre peut également être représenté sous la forme d'un StackTraceElement (voir API Java pour
StackTraceElement ). Vous pouvez en savoir plus sur la mémoire allouée à chaque thread
ici . Si nous regardons
l'API Java et recherchons le mot Thread, nous verrons qu'il existe une classe
java.lang.Thread . C'est cette classe qui représente un flux en Java, et c'est avec elle qu'il faut travailler.
java.lang.Thread
Un thread en Java est représenté comme une instance de la classe
java.lang.Thread
. Il convient de comprendre immédiatement que les instances de la classe Thread en Java ne sont pas elles-mêmes des threads. Il s'agit simplement d'une sorte d'API pour les threads de bas niveau gérés par la JVM et le système d'exploitation. Lorsque nous lançons la JVM à l'aide du lanceur Java, elle crée un thread principal avec un nom
main
et plusieurs autres threads de service. Comme indiqué dans le JavaDoc de la classe Thread :
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
Il existe 2 types de threads : les démons et les non-démons. Les threads démons sont des threads d’arrière-plan (threads de service) qui effectuent certains travaux en arrière-plan. Ce terme intéressant fait référence au « démon de Maxwell », dont vous pouvez en savoir plus dans l’article Wikipédia sur les «
démons ». Comme indiqué dans la documentation, la JVM continue d'exécuter le programme (processus) jusqu'à :
- La méthode Runtime.exit n'est pas appelée
- Tous les threads non-démons ont terminé leur travail (à la fois sans erreurs et avec exceptions levées)
D'où le détail important : les threads démons peuvent être terminés sur n'importe quelle commande en cours d'exécution. Par conséquent, l’intégrité des données qu’ils contiennent n’est pas garantie. Par conséquent, les threads démons conviennent à certaines tâches de service. Par exemple, en Java, il existe un thread responsable du traitement des méthodes de finalisation ou des threads liés au Garbage Collector (GC). Chaque thread appartient à un groupe (
ThreadGroup ). Et les groupes peuvent s'intégrer les uns aux autres, formant une hiérarchie ou une structure.
public static void main(String []args){
Thread currentThread = Thread.currentThread();
ThreadGroup threadGroup = currentThread.getThreadGroup();
System.out.println("Thread: " + currentThread.getName());
System.out.println("Thread Group: " + threadGroup.getName());
System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
Les groupes permettent de rationaliser la gestion des flux et d'en assurer le suivi. En plus des groupes, les threads ont leur propre gestionnaire d'exceptions. Regardons un exemple :
public static void main(String []args) {
Thread th = Thread.currentThread();
th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("An error occurred: " + e.getMessage());
}
});
System.out.println(2/0);
}
La division par zéro provoquera une erreur qui sera détectée par le gestionnaire. Si vous ne spécifiez pas le gestionnaire vous-même, l'implémentation du gestionnaire par défaut fonctionnera, ce qui affichera la pile d'erreurs dans StdError. Vous pouvez en savoir plus dans la revue
http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/ ". De plus, le fil de discussion a une priorité. Vous pouvez en savoir plus sur les priorités dans le article "
Priorité des threads Java en multithreading ".
Créer un fil de discussion
Comme indiqué dans la documentation, nous avons 2 façons de créer un fil de discussion. La première consiste à créer votre héritier. Par exemple:
public class HelloWorld{
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello, World!");
}
}
public static void main(String []args){
Thread thread = new MyThread();
thread.start();
}
}
Comme vous pouvez le voir, la tâche est lancée dans la méthode
run
et le thread est lancé dans la méthode
start
. Il ne faut pas les confondre, car... si nous exécutons la méthode
run
directement, aucun nouveau thread ne sera démarré. C'est la méthode
start
qui demande à la JVM de créer un nouveau thread. L'option avec un descendant de Thread est mauvaise car nous incluons Thread dans la hiérarchie des classes. Le deuxième inconvénient est que nous commençons à violer le principe de « responsabilité exclusive » SOLID, car notre classe devient simultanément responsable à la fois de la gestion du thread et de certaines tâches qui doivent être effectuées dans ce thread. Qui est correct? La réponse réside dans la méthode même
run
que nous remplaçons :
public void run() {
if (target != null) {
target.run();
}
}
En voici
target
quelques-uns
java.lang.Runnable
que nous pouvons transmettre à Thread lors de la création d’une instance de la classe. Nous pouvons donc faire ceci :
public class HelloWorld{
public static void main(String []args){
Runnable task = new Runnable() {
public void run() {
System.out.println("Hello, World!");
}
};
Thread thread = new Thread(task);
thread.start();
}
}
C'est aussi
Runnable
une interface fonctionnelle depuis Java 1.8. Cela vous permet d'écrire encore plus joliment le code de tâche pour les threads :
public static void main(String []args){
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
Total
J'espère donc que cette histoire montre clairement ce qu'est un flux, comment ils existent et quelles opérations de base peuvent être effectuées avec eux. Dans
la partie suivante , il convient de comprendre comment les threads interagissent les uns avec les autres et quel est leur cycle de vie. #Viacheslav
GO TO FULL VERSION