9 erreurs à éviter lors de la programmation en Java
Quelles sont les erreurs à éviter lors de la programmation en Java ? Dans l'article suivant, nous répondons à cette question.
Lisez la première partie de notre série de blogs consacrée à la concurrence en Java. Dans l'article suivant, nous examinerons de plus près les différences entre les threads et les processus, les pools de threads, les exécuteurs et bien d'autres choses encore !
En général, l'approche conventionnelle de la programmation est séquentielle. Dans un programme, tout se passe étape par étape.
Mais, en fait, le parallèle est le mode de fonctionnement du monde entier - c'est la capacité d'exécuter plus d'une tâche simultanément.
Pour discuter de sujets avancés tels que la concurrence dans Java ou le multithreading, nous devons nous mettre d'accord sur des définitions communes pour être sûrs d'être sur la même longueur d'onde.
Commençons par les bases. Dans le monde non séquentiel, nous disposons de deux types de représentants de la concurrence : les processus et les
threads. Un processus est une instance du programme en cours d'exécution. Normalement, il est isolé des autres processus.
Le système d'exploitation est responsable de l'attribution des ressources à chaque processus. En outre, il agit comme un chef d'orchestre qui
les planifie et les contrôle.
Le thread est une sorte de processus mais à un niveau inférieur, c'est pourquoi il est également connu sous le nom de thread léger. Plusieurs threads peuvent s'exécuter dans un
processus. Dans ce cas, le programme agit comme un planificateur et un contrôleur pour les threads. De cette manière, les programmes individuels semblent faire
plusieurs tâches en même temps.
La différence fondamentale entre les threads et les processus est le niveau d'isolation. Le processus dispose de son propre ensemble de
tandis que le thread partage des données avec d'autres threads. Cette approche peut sembler sujette aux erreurs, et c'est effectivement le cas. En effet, pour
mais ne nous attardons pas sur ce point, qui dépasse le cadre de cet article.
Processus, threads - d'accord... Mais qu'est-ce que la simultanéité ? La simultanéité signifie que vous pouvez exécuter plusieurs tâches en même temps.
temps. Cela ne signifie pas que ces tâches doivent être exécutées simultanément - c'est ce qu'on appelle le parallélisme. Concurrenc dans Javay n'est pas non plus
ne nécessite pas de disposer de plusieurs unités centrales ou même de plusieurs cœurs. Il est possible d'y parvenir dans un environnement à un seul cœur en tirant parti de la technologie
le changement de contexte.
Le multithreading est un terme lié à la concurrence. Il s'agit d'une caractéristique des programmes qui leur permet d'exécuter plusieurs tâches à la fois. Tous les programmes n'utilisent pas cette approche, mais ceux qui le font peuvent être qualifiés de multithreads.
Nous sommes presque prêts à partir, il ne reste plus qu'une définition. L'asynchronisme signifie qu'un programme effectue des opérations non bloquantes.
Il initie une tâche et passe ensuite à autre chose en attendant la réponse. Lorsqu'il reçoit la réponse, il peut y réagir.
Par défaut, chaque Application Java s'exécute dans un seul processus. Dans ce processus, il y a un fil d'exécution lié à l'application main()
méthode de
une application. Cependant, comme nous l'avons mentionné, il est possible d'exploiter les mécanismes de threads multiples au sein d'une même application.
programme.
Fil
est un Java dans laquelle la magie opère. Il s'agit de la représentation de l'objet du fil de discussion mentionné ci-dessus. Pour
créer votre propre thread, vous pouvez étendre la fonction Fil
classe. Toutefois, cette approche n'est pas recommandée. Fils
doit être utilisé comme mécanisme d'exécution de la tâche. Les tâches sont des éléments de code que nous voulons exécuter en mode concurrent. Nous pouvons les définir à l'aide de la fonction Exécutables
l'interface.
Mais trêve de théorie, joignons le geste à la parole.
Supposons que nous ayons deux tableaux de nombres. Pour chaque tableau, nous voulons connaître la somme des nombres qu'il contient. Soit
Supposons qu'il existe un grand nombre de tableaux de ce type et que chacun d'entre eux est relativement volumineux. Dans ces conditions, nous voulons utiliser la concurrence et additionner chaque tableau comme une tâche distincte.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ;
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10} ;
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7} ;
Runnable task1 = () -> {
int sum = Arrays.stream(a1).sum() ;
System.out.println("1. La somme est : " + sum) ;
} ;
Exécutable task2 = () -> {
int sum = Arrays.stream(a2).sum() ;
System.out.println("2. La somme est : " + sum) ;
} ;
Exécutable task3 = () -> {
int sum = Arrays.stream(a3).sum() ;
System.out.println("3. La somme est : " + sum) ;
} ;
new Thread(task1).start() ;
new Thread(task2).start() ;
new Thread(task3).start() ;
Comme vous pouvez le voir dans le code ci-dessus Exécutables
est une interface fonctionnelle. Elle contient une seule méthode abstraite exécuter()
sans arguments. Les Exécutables
doit être mise en œuvre par toute classe dont les instances sont censées être des
exécuté par un thread.
Une fois que vous avez défini une tâche, vous pouvez créer un thread pour l'exécuter. Pour ce faire, il suffit d'utiliser la commande nouveau Thread()
qui
prend Exécutables
comme argument.
La dernière étape consiste à démarrer()
une discussion nouvellement créée. Dans l'API, il y a aussi exécuter()
méthodes en Exécutables
et enFil
. Cependant, ce n'est pas une façon de tirer parti de la concurrence en Java. Un appel direct à chacune de ces méthodes se traduit par
l'exécution de la tâche dans le même thread que le main()
s'exécute.
Lorsqu'il y a beaucoup de tâches, il n'est pas judicieux de créer un fil de discussion distinct pour chacune d'entre elles. La création d'un Fil
est un
Il est donc préférable de réutiliser les threads existants plutôt que d'en créer de nouveaux.
Lorsqu'un programme crée de nombreux threads de courte durée, il est préférable d'utiliser un pool de threads. Le pool de threads contient un certain nombre de
les fils de discussion prêts à être exécutés mais qui ne sont pas encore actifs. L'attribution d'un Exécutables
au pool, l'un des threads appelle la fonctionexécuter()
d'une méthode donnée Exécutables
. Après avoir terminé une tâche, le thread existe toujours et se trouve dans un état d'inactivité.
Ok, vous l'avez compris - préférez le pool de threads à la création manuelle. Mais comment utiliser les pools de threads ? Les Exécuteurs
possède un certain nombre de méthodes d'usine statiques pour construire des pools de threads. Par exemple, la méthode newCachedThredPool()
crée
un pool dans lequel de nouveaux threads sont créés en fonction des besoins et où les threads inactifs sont conservés pendant 60 secondes. En revanche,newFixedThreadPool()
contient un ensemble fixe de threads, dans lequel les threads inactifs sont conservés indéfiniment.
Voyons comment cela pourrait fonctionner dans notre exemple. Désormais, nous n'avons plus besoin de créer des fils de discussion manuellement. Au lieu de cela, nous devons créerExecutorService
qui fournit un pool de threads. Nous pouvons ensuite lui assigner des tâches. La dernière étape consiste à fermer le thread
pour éviter les fuites de mémoire. Le reste du code précédent reste inchangé.
ExecutorService executor = Executors.newCachedThreadPool() ;
executor.submit(task1) ;
executor.submit(task2) ;
executor.submit(task3) ;
executor.shutdown() ;
Exécutables
semble être un moyen astucieux de créer des tâches concurrentes, mais il présente un défaut majeur. Elle ne peut pas renvoyer de
valeur. De plus, nous ne pouvons pas déterminer si une tâche est terminée ou non. Nous ne savons pas non plus si elle est terminée
normalement ou exceptionnellement. La solution à ces maux est Appelable
.
Appelable
est similaire à Exécutables
d'une certaine manière, il englobe également les tâches asynchrones. La principale différence est qu'il est capable de
renvoie une valeur. La valeur de retour peut être de n'importe quel type (non primitif) comme l'indique l'attribut Appelable
est un type paramétré.Appelable
est une interface fonctionnelle qui a call()
qui peut déclencher un Exception
.
Voyons maintenant comment nous pouvons tirer parti de l'effet de levier Appelable
dans notre problème de tableau.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ;
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10} ;
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7} ;
Callable task1 = () -> Arrays.stream(a1).sum() ;
Callable task2 = () -> Arrays.stream(a2).sum() ;
Callable task3 = () -> Arrays.stream(a3).sum() ;
ExecutorService executor = Executors.newCachedThreadPool() ;
Future future1 = executor.submit(task1) ;
Future future2 = executor.submit(task2) ;
Future future3 = executor.submit(task3) ;
System.out.println("1. La somme est : " + future1.get()) ;
System.out.println("2. la somme est : " + future2.get()) ;
System.out.println("3. la somme est : " + future3.get()) ;
executor.shutdown() ;
D'accord, nous pouvons voir comment Appelable
est créé et soumis à l'autorité compétente de l ExecutorService
. Mais qu'est-ce que c'est que L'avenir
?L'avenir
agit comme un pont entre les threads. La somme de chaque tableau est produite dans un thread séparé et nous avons besoin d'un moyen pour
de renvoyer ces résultats à main()
.
Pour récupérer le résultat de L'avenir
nous devons appeler get()
méthode. Ici, deux choses peuvent se produire. Premièrement, la méthode
le résultat du calcul effectué par Appelable
est disponible. Nous l'obtenons alors immédiatement. Deuxièmement, le résultat n'est pas
encore prêt. Dans ce cas, les get()
se bloquera jusqu'à ce que le résultat soit disponible.
Le problème avec L'avenir
est qu'il fonctionne selon le "paradigme de la poussée". Lorsque l'on utilise L'avenir
il faut être comme un patron qui
demande constamment : "Votre tâche est-elle accomplie ? Est-elle prête ?" jusqu'à ce qu'elle fournisse un résultat. Agir sous une pression constante, c'est
coûteux. La meilleure solution serait de commander L'avenir
ce qu'il doit faire lorsqu'il est prêt à accomplir sa tâche. Malheureusement,L'avenir
ne peut pas le faire, mais ComputableFuture
peut.
ComputableFuture
fonctionne selon le paradigme "pull". Nous pouvons lui dire ce qu'il doit faire du résultat lorsqu'il a accompli ses tâches. Il
est un exemple d'approche asynchrone.
ComputableFuture
fonctionne parfaitement avec Exécutables
mais pas avec Appelable
. Au lieu de cela, il est possible de fournir une tâche àComputableFuture
sous forme de Fournisseur
.
Voyons comment ce qui précède s'applique à notre problème.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ;
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10} ;
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7} ;
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println) ;
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println) ;
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println) ;
La première chose qui frappe, c'est que cette solution est beaucoup plus courte. En outre, elle a un aspect soigné et ordonné.
Tâche à CompletableFuture
peut être fournie par supplyAsync()
qui prend en compte la méthode Fournisseur
ou par runAsync()
que
prend Exécutables
. Un rappel - un morceau de code qui doit être exécuté à la fin d'une tâche - est défini par thenAccept()
méthode.
Java propose un grand nombre d'approches différentes de la concurrence. Dans cet article, nous n'avons fait qu'effleurer le sujet.
Néanmoins, nous avons couvert les bases de Fil
, Exécutables
, Appelable
et CallableFuture
ce qui constitue un bon point
pour un examen plus approfondi du sujet.