Introduction
Les fils sont une chose intéressante. Dans les revues précédentes, nous avons examiné certains des outils disponibles pour implémenter le multithreading. Voyons quelles autres choses intéressantes nous pouvons faire. À ce stade, nous en savons beaucoup. Par exemple, à partir de «
Mieux ensemble : Java et la classe Thread. Partie I — Threads d'exécution », nous savons que la classe Thread représente un thread d'exécution. Nous savons qu'un thread effectue une tâche. Si nous voulons que nos tâches puissent
run
, nous devons marquer le fil avec
Runnable
.
![Mieux ensemble : Java et la classe Thread. Partie VI — Tirez ! - 1]()
Pour rappel, nous pouvons utiliser le
compilateur Java en ligne de Tutorialspoint :
public static void main(String[] args){
Runnable task = () -> {
Thread thread = Thread.currentThread();
System.out.println("Hello from " + thread.getName());
};
Thread thread = new Thread(task);
thread.start();
}
Nous savons aussi que nous avons quelque chose qui s'appelle un verrou. Nous avons appris cela dans "
Better together: Java and the Thread class. Part II — Synchronization . Si un thread acquiert un verrou, alors un autre thread essayant d'acquérir le verrou sera obligé d'attendre que le verrou soit libéré :
import java.util.concurrent.locks.*;
public class HelloWorld{
public static void main(String []args){
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
Thread thread = Thread.currentThread();
System.out.println("Hello from " + thread.getName());
lock.unlock();
};
Thread thread = new Thread(task);
thread.start();
}
}
Je pense qu'il est temps de parler des autres choses intéressantes que nous pouvons faire.
Sémaphores
Le moyen le plus simple de contrôler le nombre de threads pouvant s'exécuter simultanément est un sémaphore. C'est comme un signal ferroviaire. Vert signifie continuer. Rouge signifie attendre. Attendre quoi du sémaphore ? Accès. Pour y avoir accès, il faut l'acquérir. Et lorsque l'accès n'est plus nécessaire, nous devons le donner ou le libérer. Voyons comment cela fonctionne. Nous devons importer la
java.util.concurrent.Semaphore
classe. Exemple:
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
Runnable task = () -> {
try {
semaphore.acquire();
System.out.println("Finished");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(task).start();
Thread.sleep(5000);
semaphore.release(1);
}
Comme vous pouvez le voir, ces opérations (acquire et release) nous aident à comprendre le fonctionnement d'un sémaphore. La chose la plus importante est que si nous voulons avoir accès, le sémaphore doit avoir un nombre positif de permis. Ce décompte peut être initialisé à un nombre négatif. Et nous pouvons demander (acquérir) plus d'un permis.
CountDownLatch
Le mécanisme suivant est
CountDownLatch
. Sans surprise, il s'agit d'un verrou avec un compte à rebours. Ici, nous avons besoin de l'instruction d'importation appropriée pour la
java.util.concurrent.CountDownLatch
classe. C'est comme une course à pied, où tout le monde se rassemble sur la ligne de départ. Et une fois que tout le monde est prêt, tout le monde reçoit le signal de départ en même temps et démarre simultanément. Exemple:
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);
Runnable task = () -> {
try {
countDownLatch.countDown();
System.out.println("Countdown: " + countDownLatch.getCount());
countDownLatch.await();
System.out.println("Finished");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
}
Tout d'abord, nous disons d'abord au verrou de
countDown()
. Google définit le compte à rebours comme "un acte de comptage des chiffres dans l'ordre inverse jusqu'à zéro". Et puis nous disons au verrou de
await()
, c'est-à-dire d'attendre que le compteur devienne zéro. Fait intéressant, il s'agit d'un compteur unique. La documentation Java indique: "Lorsque les threads doivent décompter à plusieurs reprises de cette manière, utilisez plutôt un CyclicBarrier". En d'autres termes, si vous avez besoin d'un compteur réutilisable, vous avez besoin d'une option différente :
CyclicBarrier
.
Barrière Cyclique
Comme son nom l'indique,
CyclicBarrier
est une barrière "réutilisable". Nous devrons importer la
java.util.concurrent.CyclicBarrier
classe. Regardons un exemple :
public static void main(String[] args) throws InterruptedException {
Runnable action = () -> System.out.println("On your mark!");
CyclicBarrier barrier = new CyclicBarrier(3, action);
Runnable task = () -> {
try {
barrier.await();
System.out.println("Finished");
} catch (BrokenBarrierException | InterruptedException e) {
e.printStackTrace();
}
};
System.out.println("Limit: " + barrier.getParties());
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
}
Comme vous pouvez le voir, le thread exécute la
await
méthode, c'est-à-dire qu'il attend. Dans ce cas, la valeur barrière diminue. La barrière est considérée comme brisée (
barrier.isBroken()
) lorsque le compte à rebours atteint zéro. Pour réinitialiser la barrière, vous devez appeler la
reset()
méthode, qui
CountDownLatch
n'en a pas.
Echangeur
Le mécanisme suivant est Exchanger. Dans ce contexte, un échange est un point de synchronisation où les choses changent être échangées ou permutées. Comme vous vous en doutez, an
Exchanger
est une classe qui effectue un échange ou une permutation. Prenons l'exemple le plus simple :
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
Runnable task = () -> {
try {
Thread thread = Thread.currentThread();
String withThreadName = exchanger.exchange(thread.getName());
System.out.println(thread.getName() + " exchanged with " + withThreadName);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(task).start();
new Thread(task).start();
}
Ici, nous commençons deux fils. Chacun d'eux exécute la méthode d'échange et attend que l'autre thread exécute également la méthode d'échange. Ce faisant, les threads échangent les arguments passés. Intéressant. Ça ne vous rappelle pas quelque chose ? Cela rappelle
SynchronousQueue
, qui est au cœur de
CachedThreadPool
. Pour plus de clarté, voici un exemple :
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<String> queue = new SynchronousQueue<>();
Runnable task = () -> {
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(task).start();
queue.put("Message");
}
L'exemple montre que lorsqu'un nouveau thread est démarré, il attendra, car la file d'attente sera vide. Et puis le thread principal place la chaîne "Message" dans la file d'attente. De plus, il s'arrêtera également jusqu'à ce que cette chaîne soit reçue de la file d'attente. Vous pouvez également lire "
SynchronousQueue vs Exchanger " pour en savoir plus sur ce sujet.
Phaseur
Nous avons gardé le meilleur pour la fin —
Phaser
. Nous devrons importer la
java.util.concurrent.Phaser
classe. Prenons un exemple simple :
public static void main(String[] args) throws InterruptedException {
Phaser phaser = new Phaser();
// By calling the register method, we register the current (main) thread as a party
phaser.register();
System.out.println("Phasecount is " + phaser.getPhase());
testPhaser(phaser);
testPhaser(phaser);
testPhaser(phaser);
// After 3 seconds, we arrive at the barrier and deregister. Number of arrivals = number of registrations = start
Thread.sleep(3000);
phaser.arriveAndDeregister();
System.out.println("Phasecount is " + phaser.getPhase());
}
private static void testPhaser(final Phaser phaser) {
// We indicate that there will be a +1 party on the Phaser
phaser.register();
// Start a new thread
new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + " arrived");
phaser.arriveAndAwaitAdvance(); // The threads register arrival at the phaser.
System.out.println(name + " after passing barrier");
}).start();
}
L'exemple illustre que lors de l'utilisation de
Phaser
, la barrière se brise lorsque le nombre d'inscriptions correspond au nombre d'arrivées à la barrière. Vous pouvez vous familiariser avec
Phaser
en lisant
cet article GeeksforGeeks .
Résumé
Comme vous pouvez le voir dans ces exemples, il existe différentes manières de synchroniser les threads. Plus tôt, j'ai essayé de me souvenir des aspects du multithreading. J'espère que les épisodes précédents de cette série ont été utiles. Certaines personnes disent que le chemin vers le multithreading commence avec le livre "Java Concurrency in Practice". Bien qu'il soit sorti en 2006, les gens disent que le livre est assez fondamental et toujours d'actualité aujourd'hui. Par exemple, vous pouvez lire la discussion ici :
Is "Java Concurrency In Practice" still valid? . Il est également utile de lire les liens dans la discussion. Par exemple, il y a un lien vers le livre
The Well-Grounded Java Developer , et nous ferons une mention particulière du
Chapter 4. Modern concurrency . Il y a aussi une revue complète sur ce sujet:
La "concurrence Java dans la pratique" est-elle toujours valable à l'ère de Java 8 ? Cet article propose également des suggestions sur ce qu'il faut lire pour vraiment comprendre ce sujet. Après cela, vous pouvez jeter un œil à un excellent livre comme
OCA/OCP Java SE 8 Programmer Practice Tests . Nous nous intéressons au second acronyme : OCP (Oracle Certified Professional). Vous trouverez des tests dans le "Chapitre 20 : Concurrence Java". Ce livre contient à la fois des questions et des réponses avec des explications. Par exemple :
![Mieux ensemble : Java et la classe Thread. Partie VI — Tirez ! - 3]()
Beaucoup de gens pourraient commencer à dire que cette question est encore un autre exemple de mémorisation de méthodes. D'un côté, oui. En revanche, vous pourriez répondre à cette question en rappelant qu'il
ExecutorService
s'agit d'une sorte de "mise à jour" de
Executor
. Et
Executor
est destiné à masquer simplement la façon dont les threads sont créés, mais ce n'est pas le moyen principal de les exécuter, c'est-à-dire de démarrer un
Runnable
objet sur un nouveau thread. C'est pourquoi il n'y a pas
execute(Callable)
— parce que dans
ExecutorService
, le
Executor
ajoute simplement
submit()
des méthodes qui peuvent renvoyer un
Future
objet. Bien sûr, nous pouvons mémoriser une liste de méthodes, mais il est beaucoup plus facile de faire notre réponse en fonction de notre connaissance de la nature des classes elles-mêmes. Et voici quelques documents supplémentaires sur le sujet :
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 IV — Callable, Future et friends Mieux ensemble : Java et la classe Thread. Partie V — Executor, ThreadPool, Fork/Join
GO TO FULL VERSION