Vergleich aller paralleler Mechanismen
Artikelserie: Multithreading in Java
ℹ️ Notiz
Diese Artikelserie beschreibt alle Multithreading-Mechanismen in Java anhand eines übergeordneten Beispiels. Es wird keinerlei Vorwissen über Threads vorausgesetzt, jedoch sollte man mit grundsätzlichen Sprachkonstrukten in Java (Bedingungen, Schleifen, Funktionen & Objekten) vertraut sein.
Der “Mapper” | Anwendungsbeispiel
Vor einiger Zeit hatte ich die Aufgabe, ein Programm zu schreiben, welches Dateien und Metadaten von einer Schnittstelle herunterlädt, die Metadaten umformatiert und das Ganze an eine andere Schnittstelle weitergibt. Anwendungsfall war es, zwei Plattformen von unterschiedlichen Unternehmen zu verknüpfen, da es bisher Aufgabe der Sachbearbeiter war, manuell die Daten von A nach B zu schieben, um auf der Zielplattform damit weiterarbeiten zu können.
Der Grundaufbau des Programmes war dabei recht simpel:
- Ermittle die neusten Datensätze von der Quellplattform.
(Zu einem Datensatz gehören Metadaten und mehrere verknüpfte Dateien) - Wenn es Datensätze gibt, die noch nicht bearbeitet wurden:
- Lade jeweils alle Dateien herunter, die zu dem Datensatz gehören.
- Ändere die Formatierung der Metadaten.
- Erstelle auf der Zielplattform eine neue Ressource aus den geänderten Metadaten.
- Lade alle Dateien auf der Zielplattform hoch, sodass sie mit der erstellten Ressource verknüpft sind.
(Natürlich gehört hier noch Authentifizierung auf beiden Plattformen, Fehlerbehandlung, Logging und Persistenz hinzu, man muss sich ja auch merken, welche Datensätze man bereits bearbeitet hat, aber für das Beispiel lasse ich dies einfach mal aus.)
Jetzt könnte man natürlich einen naiven Ansatz schreiben:
while (true) {
List<SourceDataset> newestData = fetchNewestData();
for (var sourceDataset : newestData) {
if (!sourceDataset.isMapped()) {
mapDataset(sourceDataset);
}
}
TimeUnit.MINUTES.sleep(5);
}
static void mapDataset(SourceDataset sourceDataset) {
List<File> downloadedFiles = fetchFiles(sourceDataset);
TargetDataset formattedDataset = reformatDataset(sourceDataset);
TargetResource targetResource = uploadDataset(formattedDataset);
for (var file : downloadedFiles)
targetResource.uploadAndAttach(file);
sourceDataset.setIsMapped();
}
…aber es gab ein klitzekleines Problem: Wir reden nicht von 10 oder 20 Datensätzen, sondern von tausenden, wobei jeder im Schnitt etwa 40 Dateien enthielt. Es war also wichtig, dass das Programm so performant wie möglich läuft und die Dateien einigermaßen gleichmäßig auf der Zielplattform ankommen. Hätte das Program, wie oben beschrieben, einfach nacheinander die Datensätze übertragen, lägen manche davon einige Stunden lang in der Warteschlange. Es wäre nicht schneller, als wenn der Sachbearbeiter es, wie gewohnt manuell macht. Aber das ist ja nicht der Sinn der Sache.
Es war recht offensichtlich, dass man viele der Aufgaben gleichzeitig erledigen könnte, zum Beispiel das Hoch- und Herunterladen von Dateien, oder überhaupt die Ausführung von zwei verschiedenen Mapping-Prozessen. Außerdem hatte ich mehrere CPU-Kerne zur Verfügung: Das obige Programm würde bisher jedoch nur einen davon benutzen, ich musste meinen Code also parallelisieren.
📖 Parallelisierung
… ist der Prozess ein Computerprogramm in mehrere unabhängige Prozesse zu unterteilen, welche man gleichzeitig ausführen kann. Man erhofft sich bei vielen oder langsamen Aufgaben einen höheren Durchsatz, also mehr absolvierte Aufgaben in einer gewissen Zeitspanne.Bei verschiedenen Programmsträngen (Threads), die man gleichzeitig ausführt, spricht man auch häufig von Multithreading.
Java bietet viele verschiedene Werkzeuge und Mechanismen um Programme zu parallelisieren, aber bei all den Möglichkeiten stellt sich die Frage: “Welcher Multithreading-Mechanismus ist am besten für mein Problem geeignet?”
Die Artikelserie
Diese Artikelserie hat den Anspruch, alle Multithreading-Mechanismen bis Java 25 einführend zu erklären. Es geht hierbei um reine Parallelität, also die gleichzeitige Ausführung von komplett unabhängigen Aufgaben. Es wird kein Vorwissen über Threads vorausgesetzt, jedoch sollte man eine grundsätzliche Erfahrung mit Java haben. Die Codebeispiele sind absichtlich vereinfacht, andere Sprachkonstrukte werden nur minimal erklärt.
Das Teilen von Ressourcen zwischen Threads, Synchronisierung, Threadsicherheit, Atomizität und Locking-Mechanismen sind ausdrücklich kein Bestandteil dieser Serie; sie sind hierbei nicht relevant und werden maximal beiläufig erwähnt.
Falls du schon Erfahrung mit Threads hast, kannst du dir unten einen beliebigen Artikel ansehen. Ansonsten klären wir erst einmal die Frage: “Was ist überhaupt ein Thread?”