Vue d’ensemble de java.util.concurrent

Vue d'ensemble de java.util.concurrent

1. Vue d'ensemble

Le packagejava.util.concurrent fournit des outils pour créer des applications simultanées.

Dans cet article, nous allons faire un survol de l’ensemble.

2. Composants principaux

Lejava.util.concurrent contient beaucoup trop de fonctionnalités à discuter en une seule écriture. Dans cet article, nous allons nous concentrer principalement sur certains des utilitaires les plus utiles de ce paquet, tels que:

  • Exécuteur

  • ExecutorService

  • ScheduledExecutorService

  • Futur

  • CountDownLatch

  • CyclicBarrier

  • Sémaphore

  • ThreadFactory

  • BlockingQueue

  • DelayQueue

  • Serrures

  • Phaser

Vous pouvez également trouver de nombreux articles consacrés à des classes individuelles ici.

2.1. Executor

Executor est une interface qui représente un objet qui exécute les tâches fournies.

Cela dépend de l'implémentation particulière (à partir du lieu où l'invocation est lancée) si la tâche doit être exécutée sur un thread nouveau ou en cours. Par conséquent, en utilisant cette interface, nous pouvons découpler le flux d’exécution de la tâche du mécanisme d’exécution de la tâche.

Un point à noter ici est queExecutor n'exige pas strictement que l'exécution de la tâche soit asynchrone. Dans le cas le plus simple, un exécuteur peut invoquer la tâche soumise instantanément dans le thread appelant.

Nous devons créer un invocateur pour créer l'instance de l'exécuteur:

public class Invoker implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

Nous pouvons maintenant utiliser cet appelant pour exécuter la tâche.

public void execute() {
    Executor executor = new Invoker();
    executor.execute( () -> {
        // task to be performed
    });
}

Il convient de noter ici que si l’exécuteur ne peut pas accepter la tâche pour l’exécution, il lanceraRejectedExecutionException.

2.2. ExecutorService

ExecutorService est une solution complète pour le traitement asynchrone. Il gère une file d'attente en mémoire et planifie les tâches soumises en fonction de la disponibilité des threads.

Pour utiliserExecutorService,, nous devons créer une classeRunnable.

public class Task implements Runnable {
    @Override
    public void run() {
        // task details
    }
}

Nous pouvons maintenant créer l'instanceExecutorService et attribuer cette tâche. Au moment de la création, nous devons spécifier la taille du pool de threads.

ExecutorService executor = Executors.newFixedThreadPool(10);

Si nous voulons créer une instanceExecutorService à thread unique, nous pouvons utilisernewSingleThreadExecutor(ThreadFactory threadFactory) pour créer l'instance.

Une fois que l'exécuteur est créé, nous pouvons l'utiliser pour soumettre la tâche.

public void execute() {
    executor.submit(new Task());
}

Nous pouvons également créer l'instanceRunnable lors de la soumission de la tâche.

executor.submit(() -> {
    new Task();
});

Il est également livré avec deux méthodes de terminaison d’exécution prêtes à l’emploi. Le premier estshutdown(); il attend la fin de l'exécution de toutes les tâches soumises. L'autre méthode estshutdownNow() quih met immédiatement fin à toutes les tâches en attente / en cours d'exécution.

Il existe également une autre méthodeawaitTermination(long timeout, TimeUnit unit) qui bloque de force jusqu'à ce que toutes les tâches aient terminé l'exécution après un événement d'arrêt déclenché ou un délai d'exécution s'est produit, ou le thread d'exécution lui-même est interrompu

try {
    executor.awaitTermination( 20l, TimeUnit.NANOSECONDS );
} catch (InterruptedException e) {
    e.printStackTrace();
}

2.3. ScheduledExecutorService

ScheduledExecutorService est une interface similaire àExecutorService, mais il peut effectuer des tâches périodiquement.

Executor and ExecutorService‘s methods are scheduled on the spot without introducing any artificial delay. Zéro ou toute valeur négative signifie que la demande doit être exécutée instantanément.

Nous pouvons utiliser les interfacesRunnable etCallable pour définir la tâche.

public void execute() {
    ScheduledExecutorService executorService
      = Executors.newSingleThreadScheduledExecutor();

    Future future = executorService.schedule(() -> {
        // ...
        return "Hello world";
    }, 1, TimeUnit.SECONDS);

    ScheduledFuture scheduledFuture = executorService.schedule(() -> {
        // ...
    }, 1, TimeUnit.SECONDS);

    executorService.shutdown();
}

ScheduledExecutorService peut également planifier la tâcheafter some given fixed delay:

executorService.scheduleAtFixedRate(() -> {
    // ...
}, 1, 10, TimeUnit.SECONDS);

executorService.scheduleWithFixedDelay(() -> {
    // ...
}, 1, 10, TimeUnit.SECONDS);

Ici, la méthodescheduleAtFixedRate( Runnable command, long initialDelay, long period, TimeUnit unit ) crée et exécute une action périodique qui est appelée d'abord après le délai initial fourni, puis avec la période donnée jusqu'à l'arrêt de l'instance de service.

La méthodescheduleWithFixedDelay( Runnable command, long initialDelay, long delay, TimeUnit unit ) crée et exécute une action périodique qui est appelée d'abord après le délai initial fourni, et de manière répétée avec le délai donné entre la fin de l'exécution et l'appel de la suivante.

2.4. Future

Future is used to represent the result of an asynchronous operation. Il est livré avec des méthodes pour vérifier si l'opération asynchrone est terminée ou non, obtenir le résultat calculé, etc.

De plus, l’APIcancel(boolean mayInterruptIfRunning) annule l’opération et libère le thread en cours d’exécution. Si la valeur demayInterruptIfRunning est vraie, le thread exécutant la tâche sera arrêté instantanément.

Sinon, les tâches en cours seront autorisées à se terminer.

Nous pouvons utiliser l'extrait de code ci-dessous pour créer une instance future:

public void invoke() {
    ExecutorService executorService = Executors.newFixedThreadPool(10);

    Future future = executorService.submit(() -> {
        // ...
        Thread.sleep(10000l);
        return "Hello world";
    });
}

Nous pouvons utiliser l'extrait de code suivant pour vérifier si le résultat futur est prêt et récupérer les données si le calcul est effectué:

if (future.isDone() && !future.isCancelled()) {
    try {
        str = future.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

Nous pouvons également spécifier un délai d'attente pour une opération donnée. Si la tâche prend plus de temps, unTimeoutException est lancé:

try {
    future.get(10, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
    e.printStackTrace();
}

2.5. CountDownLatch

CountDownLatch (introduit dansJDK 5) est une classe utilitaire qui bloque un ensemble de threads jusqu'à la fin d'une opération.

UnCountDownLatch est initialisé avec un typecounter(Integer); ce compteur décrémente à mesure que les threads dépendants terminent l'exécution. Mais une fois que le compteur atteint zéro, les autres threads sont libérés.

Vous pouvez en savoir plus surCountDownLatchhere.

2.6. CyclicBarrier

CyclicBarrier fonctionne presque de la même manière queCountDownLatch sauf que nous pouvons le réutiliser. Contrairement àCountDownLatch, il permet à plusieurs threads de s’attendre les uns les autres en utilisant la méthodeawait() (connue sous le nom de condition de barrière) avant d’appeler la tâche finale.

Nous devons créer une instance de tâcheRunnable pour lancer la condition de barrière:

public class Task implements Runnable {

    private CyclicBarrier barrier;

    public Task(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    @Override
    public void run() {
        try {
            LOG.info(Thread.currentThread().getName() +
              " is waiting");
            barrier.await();
            LOG.info(Thread.currentThread().getName() +
              " is released");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

}

Nous pouvons maintenant invoquer certains threads pour courir vers la condition de barrière:

public void start() {

    CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
        // ...
        LOG.info("All previous tasks are completed");
    });

    Thread t1 = new Thread(new Task(cyclicBarrier), "T1");
    Thread t2 = new Thread(new Task(cyclicBarrier), "T2");
    Thread t3 = new Thread(new Task(cyclicBarrier), "T3");

    if (!cyclicBarrier.isBroken()) {
        t1.start();
        t2.start();
        t3.start();
    }
}

Ici, la méthodeisBroken() vérifie si l'un des threads a été interrompu pendant le temps d'exécution. Nous devrions toujours effectuer cette vérification avant d'effectuer le processus réel.

2.7. Semaphore

LeSemaphore est utilisé pour bloquer l'accès au niveau du thread à une partie de la ressource physique ou logique. Un sémaphore contient un ensemble de permis; chaque fois qu'un thread tente d'entrer dans la section critique, il doit vérifier le sémaphore si un permis est disponible ou non.

Si un permis n'est pas disponible (viatryAcquire()), le thread n'est pas autorisé à sauter dans la section critique; cependant, si le permis est disponible, l'accès est accordé et le compteur de permis diminue.

Une fois que le thread en cours d'exécution libère la section critique, le compteur de permis augmente à nouveau (fait par la méthoderelease()).

Nous pouvons spécifier un délai pour l'acquisition de l'accès en utilisant la méthodetryAcquire(long timeout, TimeUnit unit).

Nous pouvons également vérifier le nombre de permis disponibles ou le nombre de threads en attente d'acquérir le sémaphore.

L'extrait de code suivant peut être utilisé pour implémenter un sémaphore:

static Semaphore semaphore = new Semaphore(10);

public void execute() throws InterruptedException {

    LOG.info("Available permit : " + semaphore.availablePermits());
    LOG.info("Number of threads waiting to acquire: " +
      semaphore.getQueueLength());

    if (semaphore.tryAcquire()) {
        try {
            // ...
        }
        finally {
            semaphore.release();
        }
    }

}

Nous pouvons implémenter une structure de données commeMutex en utilisantSemaphore. Plus de détails sur cecan be found here.

2.8. ThreadFactory

Comme son nom l'indique,ThreadFactory agit comme un pool de threads (non existant) qui crée un nouveau thread à la demande. Cela élimine le besoin de beaucoup de codage passe-partout pour la mise en œuvre de mécanismes efficaces de création de threads.

On peut définir unThreadFactory:

public class exampleThreadFactory implements ThreadFactory {
    private int threadId;
    private String name;

    public exampleThreadFactory(String name) {
        threadId = 1;
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, name + "-Thread_" + threadId);
        LOG.info("created new thread with id : " + threadId +
            " and name : " + t.getName());
        threadId++;
        return t;
    }
}

Nous pouvons utiliser cette méthodenewThread(Runnable r) pour créer un nouveau thread à l'exécution:

exampleThreadFactory factory = new exampleThreadFactory(
    "exampleThreadFactory");
for (int i = 0; i < 10; i++) {
    Thread t = factory.newThread(new Task());
    t.start();
}

2.9. BlockingQueue

Dans la programmation asynchrone, l'un des modèles d'intégration les plus courants est leproducer-consumer pattern. Le packagejava.util.concurrent est livré avec une structure de données connue sous le nom deBlockingQueue - qui peut être très utile dans ces scénarios asynchrones.

Plus d'informations et un exemple de travail à ce sujet sont disponibleshere.

2.10. DelayQueue

DelayQueue est une file d'attente de blocage d'éléments de taille infinie dans laquelle un élément ne peut être extrait que si son délai d'expiration (appelé délai défini par l'utilisateur) est écoulé. Par conséquent, l'élément le plus haut (head) aura le plus de retard et il sera interrogé en dernier.

Plus d'informations et un exemple de travail à ce sujet sont disponibleshere.

2.11. Locks

Sans surprise,Lock est un utilitaire pour empêcher d'autres threads d'accéder à un certain segment de code, en dehors du thread qui l'exécute actuellement.

La principale différence entre un bloc verrouillé et un bloc synchronisé réside dans le fait que le bloc synchronisé est entièrement contenu dans une méthode; Cependant, nous pouvons avoir les opérations lock () et unlock () de Lock API dans des méthodes séparées.

Plus d'informations et un exemple de travail à ce sujet sont disponibleshere.

2.12. Phaser

Phaser est une solution plus flexible queCyclicBarrier etCountDownLatch - utilisée pour agir comme une barrière réutilisable sur laquelle le nombre dynamique de threads doit attendre avant de continuer l'exécution. Nous pouvons coordonner plusieurs phases d'exécution, en réutilisant une instancePhaser pour chaque phase du programme.

Plus d'informations et un exemple de travail à ce sujet sont disponibleshere.

3. Conclusion

Dans cet article de présentation de haut niveau, nous nous sommes concentrés sur les différents utilitaires disponibles du packagejava.util.concurrent.

Comme toujours, le code source complet est disponibleover on GitHub.