Ist der Thread ExecutorService (speziell ThreadPoolExecutor) sicher?

77

Ist die ExecutorServiceGarantie Thread - Sicherheit?

Ich werde Jobs von verschiedenen Threads an denselben ThreadPoolExecutor senden. Muss ich den Zugriff auf den Executor synchronisieren, bevor ich Aufgaben interagiere / sende?

Leeeroy
quelle

Antworten:

31

Es ist wahr, die fraglichen JDK-Klassen scheinen keine explizite Garantie für die Übermittlung von threadsicheren Aufgaben zu geben. In der Praxis sind jedoch alle ExecutorService-Implementierungen in der Bibliothek auf diese Weise tatsächlich threadsicher. Ich denke, es ist vernünftig, sich darauf zu verlassen. Da der gesamte Code, der diese Funktionen implementiert, öffentlich zugänglich gemacht wurde, gibt es für niemanden eine Motivation, ihn auf eine andere Weise vollständig neu zu schreiben.

Kevin Bourrillion
quelle
"öffentlich zugänglich" wirklich? Ich dachte, es verwendet die GPL.
Raedwald
1
Das JDK tut es, Doug Lea jedoch nicht.
Kevin Bourrillion
7
Es gibt eine ausreichende Garantie für die Übermittlung von threadsicheren Aufgaben: Siehe unten im Javadoc für interface ExecutorService, die ebenfalls ThreadPoolExecutoreingehalten werden muss. (Weitere Details in meiner kürzlich aktualisierten Antwort.)
Luke Usherwood
Wenn Sie einen Executor mit einem Thread haben und in diesem Thread die Arbeit an diesen Executor senden möchten, warten Sie, bis sie abgeschlossen ist. Es tritt ein Deadlock-Problem auf, bei dem die übermittelte Arbeit niemals ausgeführt werden kann. Bei einem synchronisierten Block wird die Sperre aufgehoben, wenn Sie in den Wartemodus wechseln. Stellen Sie sich den Fall vor, in dem jemand darauf wartet, dass Ihre Aufgabe abgeschlossen ist, und in dieser Aufgabe können Sie anhand einiger Kriterien mehr Arbeit planen. Sie müssten dann warten, bis sie abgeschlossen sind, um dem ursprünglichen Anrufer zu signalisieren, wann die Arbeit tatsächlich erledigt ist.
mmm
Dies gilt für Executoren jeder Größe.
mmm
56

( Im Gegensatz zu anderen Antworten) der Thread-Sicherheit Vertrag wird dokumentiert: Blick in dem interfacejavadocs (im Gegensatz zu javadoc von Methoden gegen). Am Ende des ExecutorService- Javadocs finden Sie beispielsweise:

Auswirkungen auf die Speicherkonsistenz: Aktionen in einem Thread vor der Übermittlung einer ausführbaren oder aufrufbaren Aufgabe an einen ExecutorService werden ausgeführt , bevor alle von dieser Aufgabe ausgeführten Aktionen ausgeführt werden, bevor das Ergebnis über Future.get () abgerufen wird.

Dies reicht aus, um dies zu beantworten:

"Muss ich den Zugriff auf den Executor synchronisieren, bevor ich Aufgaben interagiere / sende?"

Nein, tust du nicht. Es ist in Ordnung, Jobs ExecutorServiceohne externe Synchronisation zu erstellen und an alle (korrekt implementierten) zu senden . Dies ist eines der wichtigsten Designziele.

ExecutorServiceist ein gleichzeitiges Dienstprogramm, das heißt, es ist so konzipiert, dass es für die Leistung zum größten Teil ohne Synchronisierung arbeitet. (Die Synchronisierung führt zu Thread-Konflikten, die die Multithreading-Effizienz beeinträchtigen können - insbesondere beim Skalieren auf eine große Anzahl von Threads.)

Es gibt keine Garantie dafür, zu welchem ​​Zeitpunkt in der Zukunft die Aufgaben ausgeführt oder abgeschlossen werden (einige werden möglicherweise sogar sofort auf demselben Thread ausgeführt, der sie gesendet hat). Es wird jedoch garantiert, dass der Arbeitsthread alle Auswirkungen gesehen hat, die der übermittelnde Thread bis zum ausgeführt hat Einreichungspunkt . Daher kann (der Thread, der ausgeführt wird) Ihre Aufgabe auch alle Daten, die für ihre Verwendung erstellt wurden, ohne Synchronisierung, threadsichere Klassen oder andere Formen der "sicheren Veröffentlichung" sicher lesen. Das Übermitteln der Aufgabe reicht selbst für eine "sichere Veröffentlichung" der Eingabedaten an die Aufgabe aus. Sie müssen nur sicherstellen, dass die Eingabedaten während der Ausführung der Aufgabe in keiner Weise geändert werden.

Wenn Sie das Ergebnis der Aufgabe über Future.get()zurückrufen, werden dem abrufenden Thread garantiert alle vom Worker-Thread des Executors vorgenommenen Effekte angezeigt (sowohl im zurückgegebenen Ergebnis als auch in den vom Worker-Thread vorgenommenen Änderungen an Nebenwirkungen). .

Dieser Vertrag impliziert auch, dass es für die Aufgaben selbst in Ordnung ist, mehr Aufgaben einzureichen.

"Garantiert der ExecutorService die Thread-Sicherheit?"

Jetzt ist dieser Teil der Frage viel allgemeiner. Zum Beispiel konnte keine Aussage eines Thread-Sicherheitsvertrags über die Methode gefunden werden shutdownAndAwaitTermination- obwohl ich feststelle, dass das Codebeispiel im Javadoc keine Synchronisation verwendet. (Obwohl es vielleicht eine versteckte Annahme gibt, dass das Herunterfahren von demselben Thread ausgelöst wird, der den Executor erstellt hat, und nicht von einem Arbeitsthread?)

Übrigens würde ich das Buch "Java Concurrency In Practice" für einen guten Einblick in die Welt der gleichzeitigen Programmierung empfehlen.

Luke Usherwood
quelle
3
Dies sind keine vollständigen Garantien für die Gewindesicherheit. Sie legen nur unter bestimmten Umständen eine Sichtbarkeitsreihenfolge fest. Beispielsweise gibt es keine explizit dokumentierte Garantie dafür, dass es sicher ist, execute () von mehreren Threads aus aufzurufen (außerhalb des Kontexts von Aufgaben, die auf dem Executor ausgeführt werden).
Meilen
1
@Miles Nach ein paar Jahren mehr Erfahrung :-) ... bin ich anderer Meinung. Die Beziehung vor dem Ereignis ist ein grundlegendes Konzept des in Java 5 eingeführten (bahnbrechenden) Java-Speichermodells, das wiederum den Grundbaustein für die Definition gleichzeitiger (im Gegensatz zu synchronisierten) Thread-Sicherheitsverträge bildet. (Und ich unterstütze wieder meine ursprüngliche Antwort, obwohl es hoffentlich jetzt mit etwas Bearbeitung klarer wird.)
Luke Usherwood
9

Ihre Frage ist eher offen: Die ExecutorServiceBenutzeroberfläche garantiert lediglich, dass irgendwo ein Thread die übermittelte Instanz Runnableoder CallableInstanz verarbeitet.

Wenn die eingereichten Runnable/ Callableverweist auf eine gemeinsam genutzte Datenstruktur , die von anderen zugänglich ist Runnable/ Callables Instanzen (möglicherweise durch verschiedene Threads unerwuenscht verarbeitet wird), dann ist es in Ihrer Verantwortung , Thread - Sicherheit in dieser Datenstruktur zu gewährleisten.

Um den zweiten Teil Ihrer Frage zu beantworten, haben Sie Zugriff auf den ThreadPoolExecutor, bevor Sie Aufgaben senden. z.B

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

BEARBEITEN

Basierend auf Brians Kommentar und für den Fall, dass ich Ihre Frage falsch verstanden habe: Die Übermittlung von Aufgaben von mehreren Produzenten-Threads an das ExecutorServiceist normalerweise threadsicher (obwohl dies, soweit ich das beurteilen kann, nicht explizit in der API der Schnittstelle erwähnt wird). Jede Implementierung, die keine Thread-Sicherheit bietet, wäre in einer Umgebung mit mehreren Threads nutzlos (da mehrere Hersteller / mehrere Verbraucher ein ziemlich verbreitetes Paradigma sind), und genau dafür wurde ExecutorService(und der Rest von java.util.concurrent) entwickelt.

Adamski
quelle
8
Ist es nicht das, was er fragt, dass die Einreichung threadsicher ist? dh dass er aus verschiedenen Threads einreichen kann
Brian Agnew
1
Ja, ich frage, ob es sicher ist, Aufgaben von mehreren Threads an dieselbe ThreadPoolExecutor-Instanz zu senden. Die Frage wurde aktualisiert, da ein wichtiges "Synchronisierungs" -Wort verschwunden ist: |
Leeeroy
1
"Jede Implementierung, die keine Thread-Sicherheit bietet, wäre in einer Umgebung mit mehreren Threads nutzlos": Es ist für einen hypothetischen ExecutorService nicht völlig unplausibel, eine nicht thread-sichere Implementierung bereitzustellen, da Single-Producer ein weit verbreitetes Muster ist. (Aber für ThreadPoolExecutor, der für den allgemeinen Gebrauch bestimmt ist, steht dieser Kommentar auf jeden Fall)
Miles
6

Für ThreadPoolExecutordie Antwort ist einfach ja . ExecutorServiceübernimmt keine Garantie oder anderweitige Garantie dafür, dass alle Implementierungen threadsicher sind, und kann dies nicht, da es sich um eine Schnittstelle handelt. Diese Vertragsarten liegen außerhalb des Bereichs einer Java-Schnittstelle. Doch ThreadPoolExecutorbeide sind und eindeutig als Thread-sicher dokumentiert. Darüber hinaus wird ThreadPoolExecutordie Jobwarteschlange über java.util.concurrent.BlockingQueueeine Schnittstelle verwaltet, die anfordert, dass alle Implementierungen threadsicher sind. Jede java.util.concurrent.*Implementierung von BlockingQueuekann sicher als threadsicher angenommen werden. Eine nicht standardmäßige Implementierung ist möglicherweise nicht möglich, obwohl dies völlig albern wäre, wenn jemand eine BlockingQueueImplementierungswarteschlange bereitstellen würde, die nicht threadsicher ist.

Die Antwort auf Ihre Titelfrage lautet also eindeutig Ja . Die Antwort auf den nachfolgenden Teil Ihrer Frage lautet wahrscheinlich , da zwischen den beiden einige Diskrepanzen bestehen.

Scott S. McCoy
quelle
4
Schnittstellen können und erfordern threadsichere Implementierungen. Thread-Sicherheit ist ein dokumentierter Vertrag, genau wie jede andere Art von Verhalten (wie List.hashCode()). Die Javadocs sagen "BlockingQueue-Implementierungen sind threadsicher" (eine nicht threadsichere BlockingQueue ist also nicht nur albern, sondern auch fehlerhaft), aber es gibt keine solche Dokumentation für ThreadPoolExecutor oder eine der von ihr implementierten Schnittstellen.
Meilen
1
Können Sie bitte auf die Dokumentation verweisen, in der eindeutig angegeben ist, dass sie ThreadPoolExecutorthreadsicher ist?
Mapeters
2

Für ThreadPoolExecutor ist die Übermittlung threadsicher. Sie können den Quellcode in jdk8 sehen. Beim Hinzufügen einer neuen Aufgabe wird ein mainLock verwendet, um die Thread-Sicherheit zu gewährleisten.

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;
        }
salexinx
quelle
1

Entgegen der Antwort von Luke Usherwood wird in der Dokumentation nicht impliziert, dass ExecutorServiceImplementierungen garantiert threadsicher sind. Bezüglich der Frage von ThreadPoolExecutorspezifisch siehe andere Antworten.

Ja, es wird eine Vor-Ort- Beziehung angegeben, dies bedeutet jedoch nichts über die Thread-Sicherheit der Methoden selbst, wie von Miles kommentiert . In der Antwort von Luke Usherwood heißt es, dass Ersteres ausreicht, um Letzteres zu beweisen, aber es wird kein tatsächliches Argument vorgebracht.

"Thread-Sicherheit" kann verschiedene Bedeutungen haben, aber hier ist ein einfaches Gegenbeispiel für eine Executor(nicht, ExecutorServiceaber es macht keinen Unterschied), die die erforderliche Vor-Ort- Beziehung trivial erfüllt, aber aufgrund des nicht synchronisierten Zugriffs auf das countFeld nicht thread-sicher ist .

class CountingDirectExecutor implements Executor {

    private int count = 0;

    public int getExecutedTaskCount() {
        return count;
    }

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

Haftungsausschluss: Ich bin kein Experte und habe diese Frage gefunden, weil ich selbst nach der Antwort gesucht habe.

user2357
quelle
Was Sie angeben, ist alles wahr, aber die Frage lautet speziell "Muss ich den Zugriff auf den Executor synchronisieren?". Daher lese ich in diesem Zusammenhang "Thread-Sicherheit", um nur über die Thread-Sicherheit von (dem Status / den Daten im Inneren) zu sprechen the) executor und die Aktionen zum Aufrufen seiner Methoden.
Luke Usherwood
Wie man eingereichte Aufgaben selbst "threadsichere Nebenwirkungen" hat, ist ein viel größeres Thema! (Es ist viel einfacher, wenn dies nicht der Fall ist. Wenn ein unveränderliches berechnetes Ergebnis einfach zurückgegeben werden kann. Wenn sie einen veränderlichen gemeinsamen Status berühren, müssen Sie darauf achten, die Thread-Grenzen zu definieren und zu verstehen und zu berücksichtigen Thread-Sicherheit, Dead-Locks, Live-Locks usw.)
Luke Usherwood