Un guide pour Java SynchronousQueue

Guide de la file d'attente synchrone Java

1. Vue d'ensemble

Dans cet article, nous allons examiner lesSynchronousQueue du packagejava.util.concurrent.

En termes simples, cette implémentation nous permet d’échanger des informations entre les threads d’une manière thread-safe.

2. Aperçu de l'API

LeSynchronousQueue n'a quetwo supported operations: take() and put(), and both of them are blocking.

Par exemple, lorsque nous voulons ajouter un élément à la file d'attente, nous devons appeler la méthodeput(). Cette méthode se bloquera jusqu'à ce qu'un autre thread appelle la méthodetake(), signalant qu'il est prêt à prendre un élément.

Bien que leSynchronousQueue ait une interface de file d'attente, nous devrions le considérer comme un point d'échange pour un seul élément entre deux threads, dans lequel un thread transfère un élément et un autre thread prend cet élément.

3. Implémentation de transferts à l'aide d'une variable partagée

Pour voir pourquoi lesSynchronousQueue peuvent être si utiles, nous implémenterons une logique utilisant une variable partagée entre deux threads et ensuite, nous réécrirons cette logique en utilisantSynchronousQueue rendant notre code beaucoup plus simple et plus lisible.

Supposons que nous ayons deux threads - un producteur et un consommateur - et lorsque le producteur définit la valeur d'une variable partagée, nous voulons signaler ce fait au thread consommateur. Ensuite, le fil consommateur va chercher une valeur à partir d'une variable partagée.

Nous utiliserons lesCountDownLatch pour coordonner ces deux threads, pour éviter une situation où le consommateur accède à une valeur d'une variable partagée qui n'a pas encore été définie.

Nous allons définir une variablesharedState et unCountDownLatch qui seront utilisés pour coordonner le traitement:

ExecutorService executor = Executors.newFixedThreadPool(2);
AtomicInteger sharedState = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(1);

Le producteur sauvegardera un entier aléatoire dans la variablesharedState, et exécutera la méthodecountDown() sur lecountDownLatch, signalant au consommateur qu'il peut récupérer une valeur dessharedState:

Runnable producer = () -> {
    Integer producedElement = ThreadLocalRandom
      .current()
      .nextInt();
    sharedState.set(producedElement);
    countDownLatch.countDown();
};

Le consommateur attendra lescountDownLatch en utilisant la méthodeawait(). Lorsque le producteur signale que la variable a été définie, le consommateur la récupérera dans lessharedState:

Runnable consumer = () -> {
    try {
        countDownLatch.await();
        Integer consumedElement = sharedState.get();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Dernier point mais non le moindre, commençons notre programme:

executor.execute(producer);
executor.execute(consumer);

executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(countDownLatch.getCount(), 0);

Il produira la sortie suivante:

Saving an element: -1507375353 to the exchange point
consumed an element: -1507375353 from the exchange point

Nous pouvons voir que c'est beaucoup de code pour implémenter une fonctionnalité aussi simple que l'échange d'un élément entre deux threads. Dans la section suivante, nous allons essayer de l'améliorer.

4. Implémentation des transferts à l'aide desSynchronousQueue

Implémentons maintenant la même fonctionnalité que dans la section précédente, mais avec unSynchronousQueue. Cela a un double effet car nous pouvons l'utiliser pour échanger l'état entre les threads et pour coordonner cette action afin que nous n'ayons pas besoin d'utiliser quoi que ce soit en plus deSynchronousQueue.

Tout d'abord, nous définirons une file d'attente:

ExecutorService executor = Executors.newFixedThreadPool(2);
SynchronousQueue queue = new SynchronousQueue<>();

Le producteur appellera une méthodeput() qui bloquera jusqu'à ce qu'un autre thread prenne un élément de la file d'attente:

Runnable producer = () -> {
    Integer producedElement = ThreadLocalRandom
      .current()
      .nextInt();
    try {
        queue.put(producedElement);
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Le consommateur va simplement récupérer cet élément en utilisant la méthodetake():

Runnable consumer = () -> {
    try {
        Integer consumedElement = queue.take();
    } catch (InterruptedException ex) {
        ex.printStackTrace();
    }
};

Ensuite, nous allons commencer notre programme:

executor.execute(producer);
executor.execute(consumer);

executor.awaitTermination(500, TimeUnit.MILLISECONDS);
executor.shutdown();
assertEquals(queue.size(), 0);

Il produira la sortie suivante:

Saving an element: 339626897 to the exchange point
consumed an element: 339626897 from the exchange point

Nous pouvons voir qu'unSynchronousQueue est utilisé comme point d'échange entre les threads, ce qui est beaucoup mieux et plus compréhensible que l'exemple précédent qui utilisait l'état partagé avec unCountDownLatch.

5. Conclusion

Dans ce rapide tutoriel, nous avons examiné la constructionSynchronousQueue. Nous avons créé un programme qui échange des données entre deux threads en utilisant l'état partagé, puis réécrit ce programme pour tirer parti de la constructionSynchronousQueue. Cela sert de point d'échange qui coordonne le fil producteur et le fil consommateur.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans leGitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.