Aufbau des Benchmarks
Es werden Szenarien mit 2.000.000 Dokumenten betrachtet, von denen jedes mit maximal drei Benutzern und Tags assoziiert ist. Ein Task ist direkt zu berechnen, falls er maximal 10.000 Dokumente umfasst. Andernfalls wird er in zwei neue Tasks mit jeweils der Hälfte der Dokumente zerlegt.
Performance-Ergebnisse des Benchmarks werden im Folgenden durch Speedup-Werte in Abhängigkeit der Anzahl verwendeter Worker Threads dargestellt (siehe Abbildung 2, 3 und 6). Der jeweilige Wert ergibt sich aus der Laufzeit des Benchmarks unter Verwendung des ThreadPoolExecutor beziehungsweise ForkJoinPool, ins Verhältnis gesetzt zur Laufzeit einer sequenziellen Berechnung mit nur einem Thread (ohne einen Pool). Jede dieser Laufzeiten berechnet sich wiederum als Mittelwert aus 20 Läufen desselben Szenarios.
Um den Einfluss des Just-In-Time-Compilers zu minimieren, wurden jedem Benchmark zehn Warm-up-Läufe in derselben JVM vorgeschaltet. Durch das Verwenden einer festen Heap-Größe von 4 GByte ließ sich der Anteil der Garbage Collection an der Gesamtlaufzeit durchweg unter 5 Prozent halten. Alle Benchmarks liefen auf einem Rechner mit 32 virtuellen Prozessoren (in Form von 16 physikalischen Cores plus Hyperthreading).
Betrachtung der Ergebnisse
Wie Abbildung 2 zeigt, können sowohl der ThreadPoolExecutor als auch der ForkJoinPool die Berechnung gegenüber der sequenziellen Variante beschleunigen. Allerdings erzielt der ThreadPoolExecutor bis zur Anzahl verfügbarer Prozessoren durchweg einen höheren Speedup. Genau solche Ergebnisse führen oft zu Verwunderung – denn das Szenario sollte eigentlich gut für Fork/Join geeignet sein.
Der ThreadPoolExecutor erzielt bis zur Anzahl der verfügbaren Prozessoren einen höheren Speedup als der ForkJoinPool (Abb. 2).
Die Erklärung für die Ergebnisse ist, dass alle Eingabe-Tasks einen ähnlichen Berechnungsaufwand mit sich bringen. Die rekursive Zerlegung und das Work Stealing des ForkJoinPool sind folglich unnötig. Im Vergleich zum ThreadPoolExecutor bleibt nur der Overhead für die Erstellung zusätzlicher Tasks. Obwohl MapReduce also eine gut geeignete Anwendung für den ForkJoinPool ist, muss das nicht heißen, dass der ForkJoinPool auch immer die beste Lösung ist. Lässt sich ein Problem zuverlässig in gleichgroße Teile partitionieren, ist der ThreadPoolExecutor (oder sogar das explizite Starten und Zusammenführen eigener Threads) oft vorzuziehen.
Man kann sich jedoch nicht unbedingt darauf verlassen, dass die Rechenlast gleichmäßig auf die Eingabe-Tasks verteilt ist. So können zum Beispiel manche Dokumente von mehr Benutzern geteilt werden oder weit mehr Tags besitzen als andere. In einer zweiten Betrachtung des Beispiels soll deshalb ein Viertel aller Dokumente mit bis zu viermal so vielen Benutzern und Tags assoziiert sein.
Bei ungleichmäßig verteilter Rechenlast schneidet der ForkJoinPool unverändert gut ab, während der ThreadPoolExecutor stark unter dem Ungleichgewicht leidet (Abb. 3).
Wie Abbildung 3 zeigt, erledigt der ForkJoinPool die Berechnung unverändert schnell – der Speedup ist sogar etwas höher, weil die Rechenlast insgesamt gestiegen ist. Der ThreadPoolExecutor hingegen leidet erheblich unter dem Ungleichgewicht. Der Grund hierfür ist, dass der ForkJoinPool durch Work Stealing automatisch für einen Lastausgleich zwischen den Threads sorgt, während der ThreadPoolExecutor keinerlei Mechanismus dafür bereitstellt.
Zum Beleg veranschaulicht Abbildung 4 die Zustände der Worker Threads während der Berechnung der Benchmarks durch den ThreadPoolExecutor mit vier Threads (gemessen mit VisualVM; grün heißt "lauffähig", gelb heißt "wartend"). Ein Thread hat stets den Großteil der Rechenlast alleine zu tragen. Das erklärt, warum der ThreadPoolExecutor bei steigender Anzahl Threads wieder etwas besser abschneidet: Das Ungleichgewicht verteilt sich dann auf mehrere Threads.
Beim ThreadPoolExecutor führt ein Thread den Großteil der Berechnung durch, während die anderen
im Ruhezustand sind (Abb. 4).
Zum Vergleich zeigt Abbildung 5, dass die Threads beim ForkJoinPool dank des Work-Stealing-Verfahrens wesentlich besser ausgelastet sind. Es lässt sich beobachten, dass zusätzliche Worker Threads gestartet werden, wenn bereits aktive Threads auf die Ergebnisse anderer Tasks warten müssen. Trotzdem sind zu jeder Zeit nur vier Worker Threads tatsächlich lauffähig, die anderen befinden sich im Ruhezustand.
Beim ForkJoinPool findet ein automatischer Lastausgleich durch Work Stealing und gegebenenfalls zusätzliche Worker Threads statt (Abb. 5).
Insgesamt lässt sich festhalten: Der ThreadPoolExecutor ist vorzuziehen, wenn die Rechenlast gleichmäßig auf die Worker Threads verteilt ist. Um das zu garantieren, ist allerdings eine genaue Kenntnis der Eingabedaten nötig. Der ForkJoinPool hingegen zeigt unabhängig von den Eingabedaten eine gute Performance und stellt damit die deutlich robustere Lösung dar.
Ab sofort kann man sich mit Vorträgen für die neue Konferenz zu Agile ALM, Continuous Delivery und DevOps bewerben.






Am 5. und 6. Juni trifft sich in Toulouse die Eclipse-Community zur Erstauflage der EclipseCon France. Bis 26. Mai kann man sich noch zum Frühbucherpreis registrieren.