web-dev-qa-db-fra.com

Le thread ExecutorService (spécifiquement ThreadPoolExecutor) est-il sûr?

Le ExecutorService garantit-il la sécurité des threads?

Je soumettrai des travaux de différents threads au même ThreadPoolExecutor, dois-je synchroniser l'accès à l'exécuteur avant d'interagir/soumettre des tâches?

72
leeeroy

C'est vrai, les classes JDK en question ne semblent pas garantir explicitement la soumission de tâches thread-safe. Cependant, dans la pratique, toutes les implémentations ExecutorService de la bibliothèque sont en effet thread-safe de cette manière. Je pense qu'il est raisonnable de dépendre de cela. Étant donné que tout le code implémentant ces fonctionnalités a été placé dans le domaine public, il n'y a absolument aucune motivation pour quiconque de le réécrire complètement d'une manière différente.

31

(Contrairement à d'autres réponses) le contrat de sécurité des threads est documenté: regardez dans les interface javadocs (par opposition à javadoc des méthodes) . Par exemple, au bas du ExecutorService javadoc vous trouvez:

Effets de cohérence de la mémoire: actions dans un thread avant la soumission d'une tâche exécutable ou appelable à un ExecutorService arrive-avant toute action prise par cette tâche, qui à son tour arrive-avant le résultat est récupéré via Future.get ().

Cela suffit pour répondre à ceci:

"dois-je synchroniser l'accès à l'exécuteur avant d'interagir/soumettre des tâches?"

Non, non. Il est correct de construire et de soumettre des travaux à n'importe quel (correctement implémenté) ExecutorService sans synchronisation externe. C'est l'un des principaux objectifs de conception.

ExecutorService est un utilitaire concurrent, c'est-à-dire qu'il est conçu pour fonctionner au maximum sans nécessiter de synchronisation, pour des performances. (La synchronisation entraîne des conflits de threads, ce qui peut dégrader l'efficacité du multithread - en particulier lors de la mise à l'échelle vers un grand nombre de threads.)

Il n'y a aucune garantie quant à l'heure à laquelle les tâches seront exécutées ou terminées à l'avenir (certaines peuvent même s'exécuter immédiatement sur le même thread qui les a soumises), mais le thread de travail est garanti d'avoir vu tous les effets que le thread de soumission a effectués jusqu'au point de soumission. Par conséquent (le thread qui s'exécute), votre tâche peut également lire en toute sécurité toutes les données créées pour son utilisation sans synchronisation, classes thread-safe ou toute autre forme de "publication sécurisée". L'acte de soumettre la tâche est lui-même suffisant pour une "publication sûre" des données d'entrée à la tâche. Il vous suffit de vous assurer que les données d'entrée ne seront en aucun cas modifiées pendant l'exécution de la tâche.

De même, lorsque vous récupérez le résultat de la tâche via Future.get(), le thread de récupération sera garanti de voir tous les effets effectués par le thread de travail de l'exécuteur (dans le résultat renvoyé, plus tout effet secondaire modifie le thread de travail peut avoir fait).

Ce contrat implique également que les tâches elles-mêmes peuvent soumettre plus de tâches.

"ExecutorService garantit-il la sécurité des threads?"

Maintenant, cette partie de la question est beaucoup plus générale. Par exemple, je n'ai trouvé aucune déclaration d'un contrat de sécurité des threads concernant la méthode shutdownAndAwaitTermination - bien que je note que l'exemple de code dans le Javadoc n'utilise pas la synchronisation. (Bien qu'il y ait peut-être une hypothèse cachée selon laquelle l'arrêt est provoqué par le même thread qui a créé l'exécuteur, et non par exemple un thread de travail?)

BTW Je recommanderais le livre "Java Concurrency In Practice" pour une bonne connaissance du monde de la programmation simultanée.

48
Luke Usherwood

Votre question est plutôt ouverte: tout ce que fait l'interface ExecutorService est de garantir qu'un thread quelque part traitera l'instance Runnable ou Callable soumise.

Si le Runnable/Callable soumis fait référence à une structure de données partagée accessible depuis d'autres instances Runnable/Callables (potentiellement en cours de traitement simultané par différents threads), alors c'est votre responsabilité d'assurer la sécurité des threads à travers cette structure de données.

Pour répondre à la deuxième partie de votre question, oui, vous aurez accès à ThreadPoolExecutor avant de soumettre des tâches; par exemple.

BlockingQueue<Runnable> workQ = new LinkedBlockingQueue<Runnable>();
ExecutorService execService = new ThreadPoolExecutor(4, 4, 0L, TimeUnit.SECONDS, workQ);
...
execService.submit(new Callable(...));

MODIFIER

Sur la base du commentaire de Brian et au cas où j'aurais mal compris votre question: la soumission des tâches de plusieurs threads producteurs au ExecutorService sera généralement thread-safe (bien que cela ne soit pas mentionné explicitement dans l'API de l'interface autant que je peux) dire). Toute implémentation qui n'offre pas la sécurité des threads serait inutile dans un environnement multi-thread (comme plusieurs producteurs/plusieurs consommateurs est un paradigme assez courant), et c'est précisément ce que ExecutorService (et le reste de Java.util.concurrent) a été conçu pour.

9
Adamski

Pour ThreadPoolExecutor, la réponse est simplement oui. ExecutorService ne pas mandat ou garantit autrement que toutes les implémentations sont thread-safe, et cela ne peut pas car c'est une interface. Ces types de contrats n'entrent pas dans le cadre d'une interface Java. Cependant, ThreadPoolExecutor est et est clairement documenté comme étant thread-safe. De plus, ThreadPoolExecutor gère sa file d'attente de travaux à l'aide de Java.util.concurrent.BlockingQueue qui est une interface qui demande que toutes les implémentations soient thread-safe. Tout Java.util.concurrent.* l'implémentation de BlockingQueue peut être considérée en toute sécurité comme thread-safe. Toute implémentation non standard peut ne pas l'être, bien que ce serait complètement idiot si quelqu'un devait fournir une file d'attente d'implémentation BlockingQueue qui n'était pas thread-safe.

La réponse à votre question de titre est donc clairement oui. La réponse au corps suivant de votre question est probablement, car il y a des différences entre les deux.

6
Scott S. McCoy

Contrairement à ce que dit la réponse de Luke Usherwood , la documentation n'implique pas que les implémentations ExecutorService sont garanties pour être thread-safe. Quant à la question de ThreadPoolExecutor en particulier, voir les autres réponses.

Oui, une relation se produit avant est spécifiée, mais cela n'implique rien sur la sécurité des threads des méthodes elles-mêmes, comme l'a commenté Miles . Dans la réponse de Luke Usherwood , il est indiqué que la première est suffisante pour prouver la seconde, mais aucun argument réel n'est avancé.

"Thread-safety" peut signifier différentes choses, mais voici un simple contre-exemple d'un Executor (pas ExecutorService mais cela ne fait aucune différence) qui satisfait trivialement le requis se produit avant mais n'est pas thread-safe en raison de l'accès non synchronisé au champ count.

class CountingDirectExecutor implements Executor {

    private int count = 0;

    public int getExecutedTaskCount() {
        return count;
    }

    public void execute(Runnable command) {
        command.run();
    }
}

Avertissement: je ne suis pas un expert et j'ai trouvé cette question parce que je cherchais moi-même la réponse.

1
user2357

Pour ThreadPoolExecutor, il soumet est thread-safe. Vous pouvez voir le code source dans jdk8. Lors de l'ajout d'une nouvelle tâche, il utilise un verrou principal pour garantir la sécurité du thread.

private boolean addWorker(Runnable firstTask, boolean core) {
            retry:
            for (;;) {
                int c = ctl.get();
                int rs = runStateOf(c);

                // Check if queue empty only if necessary.
                if (rs >= SHUTDOWN &&
                    ! (rs == SHUTDOWN &&
                       firstTask == null &&
                       ! workQueue.isEmpty()))
                    return false;

                for (;;) {
                    int wc = workerCountOf(c);
                    if (wc >= CAPACITY ||
                        wc >= (core ? corePoolSize : maximumPoolSize))
                        return false;
                    if (compareAndIncrementWorkerCount(c))
                        break retry;
                    c = ctl.get();  // Re-read ctl
                    if (runStateOf(c) != rs)
                        continue retry;
                    // else CAS failed due to workerCount change; retry inner loop
                }
            }

            boolean workerStarted = false;
            boolean workerAdded = false;
            Worker w = null;
            try {
                w = new Worker(firstTask);
                final Thread t = w.thread;
                if (t != null) {
                    final ReentrantLock mainLock = this.mainLock;
                    mainLock.lock();
                    try {
                        // Recheck while holding lock.
                        // Back out on ThreadFactory failure or if
                        // shut down before lock acquired.
                        int rs = runStateOf(ctl.get());

                        if (rs < SHUTDOWN ||
                            (rs == SHUTDOWN && firstTask == null)) {
                            if (t.isAlive()) // precheck that t is startable
                                throw new IllegalThreadStateException();
                            workers.add(w);
                            int s = workers.size();
                            if (s > largestPoolSize)
                                largestPoolSize = s;
                            workerAdded = true;
                        }
                    } finally {
                        mainLock.unlock();
                    }
                    if (workerAdded) {
                        t.start();
                        workerStarted = true;
                    }
                }
            } finally {
                if (! workerStarted)
                    addWorkerFailed(w);
            }
            return workerStarted;
        }
1
salexinx