Anleitung zum Fork/Join-Framework in Java

1. Überblick

Das Fork/Join-Framework wurde in Java 7 vorgestellt. Es bietet Werkzeuge zur Beschleunigung der parallelen Verarbeitung, indem versucht wird, alle verfügbaren Prozessorkerne zu verwenden - was durch einen Divide-and-Conquer-Ansatz ** erreicht wird.

In der Praxis bedeutet dies, dass sich das Framework zuerst „verzweigt“ , wodurch die Aufgabe rekursiv in kleinere unabhängige Unteraufgaben aufgeteilt wird, bis sie einfach genug sind, um asynchron ausgeführt zu werden.

Danach beginnt der "join" -Teil , in dem die Ergebnisse aller Unteraufgaben rekursiv zu einem einzigen Ergebnis zusammengefügt werden, oder im Falle einer Aufgabe, die void zurückgibt, wartet das Programm einfach, bis jede Unteraufgabe ausgeführt wird.

Um eine effektive parallele Ausführung zu gewährleisten, verwendet das Fork/Join-Framework einen Pool von Threads namens ForkJoinPool , der Arbeitsthreads des Typs ForkJoinWorkerThread verwaltet.

2. ForkJoinPool

Der ForkJoinPool ist das Herzstück des Frameworks. Dies ist eine Implementierung des ExecutorService , das Worker-Threads verwaltet und uns Tools zur Verfügung stellt, mit denen Informationen zum Status und zur Leistung des Thread-Pools abgerufen werden können.

Worker-Threads können jeweils nur eine Aufgabe ausführen, aber ForkJoinPool erstellt keinen separaten Thread für jede einzelne Unteraufgabe. Stattdessen verfügt jeder Thread im Pool über eine eigene Warteschlange mit zwei Enden (oder https://en.wikipedia.org/wiki/Double-ended queue[deque], die deck__ ausgesprochen wird), in der die Aufgaben gespeichert werden.

Diese Architektur ist wichtig, um die Arbeitslast des Threads mithilfe des work-stealing-Algorithmus auszugleichen.

2.1. Arbeitsstehlsalgorithmus

  • Einfach umschriebene Threads versuchen, die Arbeit von Deques belebter Threads zu "stehlen".

Standardmäßig erhält ein Worker-Thread Aufgaben vom Kopf seines eigenen Deque.

Wenn es leer ist, übernimmt der Thread eine Aufgabe vom Ende des Deque eines anderen ausgelasteten Threads oder von der globalen Eintragswarteschlange, da sich dort wahrscheinlich die größten Arbeiten befinden.

Dieser Ansatz minimiert die Möglichkeit, dass Threads um Aufgaben konkurrieren. Es reduziert auch die Häufigkeit, mit der der Thread nach Arbeit suchen muss, da er zuerst die größten verfügbaren Arbeitsschritte bearbeitet.

2.2. ForkJoinPool Instantiation

In Java 8 ist der bequemste Weg, auf die Instanz von ForkJoinPool zuzugreifen, die statische Methode () . jeder ForkJoinTask .

Gemäß der Dokumentation von Oracle verringert die Verwendung des vordefinierten gemeinsamen Pools den Ressourcenverbrauch, da dies die Erstellung eines Systems verhindert separater Thread-Pool pro Task.

ForkJoinPool commonPool = ForkJoinPool.commonPool();

Dasselbe Verhalten kann in Java 7 erreicht werden, indem ein ForkJoinPool erstellt und einem public static -Feld einer Dienstprogrammklasse zugewiesen wird:

public static ForkJoinPool forkJoinPool = new ForkJoinPool(2);

Nun ist es leicht zugänglich:

ForkJoinPool forkJoinPool = PoolUtil.forkJoinPool;

Mit ForkJoinPool’s -Konstruktoren ist es möglich, einen benutzerdefinierten Thread-Pool mit einem bestimmten Grad an Parallelität, Thread-Factory und Ausnahmebehandlung zu erstellen. Im obigen Beispiel hat der Pool eine Parallelitätsstufe von 2. Dies bedeutet, dass der Pool 2 Prozessorkerne verwendet.

3. ForkJoinTask <V>

ForkJoinTask ist der Basistyp für Aufgaben, die in ForkJoinPool ausgeführt werden. In der Praxis sollte eine der beiden Unterklassen erweitert werden: die RecursiveAction für void -Aufgaben und die RecursiveTask <V> für Aufgaben, die einen Wert zurückgeben. Sie haben beide eine abstrakte Methode compute () __, in der die Logik der Aufgabe definiert ist.

** 3.1. __RecursiveAction - Ein Beispiel

__ **

Im folgenden Beispiel wird die zu verarbeitende Arbeitseinheit durch einen String mit dem Namen workload dargestellt. Zu Demonstrationszwecken ist die Aufgabe eine unsinnige Aufgabe: Sie überlagert einfach ihre Eingabe und protokolliert sie.

Um das Gabelverhalten des Frameworks zu veranschaulichen, teilt das Beispiel die Task auf, wenn workload.length () mit der Methode createSubtask () größer als ein angegebener Schwellenwert ** __ ist.

Der String wird rekursiv in Teilstrings unterteilt, wodurch CustomRecursiveTask -Instanzen erstellt werden, die auf diesen Teilstrings basieren.

Daher gibt die Methode eine __List <CustomRecursiveAction> zurück.

Die Liste wird mit der invokeAll () - Methode an ForkJoinPool übergeben:

public class CustomRecursiveAction extends RecursiveAction {

    private String workload = "";
    private static final int THRESHOLD = 4;

    private static Logger logger =
      Logger.getAnonymousLogger();

    public CustomRecursiveAction(String workload) {
        this.workload = workload;
    }

    @Override
    protected void compute() {
        if (workload.length() > THRESHOLD) {
            ForkJoinTask.invokeAll(createSubtasks());
        } else {
           processing(workload);
        }
    }

    private List<CustomRecursiveAction> createSubtasks() {
        List<CustomRecursiveAction> subtasks = new ArrayList<>();

        String partOne = workload.substring(0, workload.length()/2);
        String partTwo = workload.substring(workload.length()/2, workload.length());

        subtasks.add(new CustomRecursiveAction(partOne));
        subtasks.add(new CustomRecursiveAction(partTwo));

        return subtasks;
    }

    private void processing(String work) {
        String result = work.toUpperCase();
        logger.info("This result - (" + result + ") - was processed by "
          + Thread.currentThread().getName());
    }
}

Dieses Muster kann verwendet werden, um eigene RecursiveAction -Klassen zu entwickeln. _. _ Erstellen Sie dazu ein Objekt, das die Gesamtmenge der Arbeit darstellt, wählen Sie einen geeigneten Schwellenwert aus, definieren Sie eine Methode, um die Arbeit aufzuteilen, und definieren Sie eine Methode, um die Arbeit auszuführen.

3.2. RecursiveTask <V>

Für Aufgaben, die einen Wert zurückgeben, ist die Logik hier ähnlich, mit der Ausnahme, dass das Ergebnis für jede Unteraufgabe in einem einzigen Ergebnis vereint wird:

public class CustomRecursiveTask extends RecursiveTask<Integer> {
    private int[]arr;

    private static final int THRESHOLD = 20;

    public CustomRecursiveTask(int[]arr) {
        this.arr = arr;
    }

    @Override
    protected Integer compute() {
        if (arr.length > THRESHOLD) {
            return ForkJoinTask.invokeAll(createSubtasks())
              .stream()
              .mapToInt(ForkJoinTask::join)
              .sum();
        } else {
            return processing(arr);
        }
    }

    private Collection<CustomRecursiveTask> createSubtasks() {
        List<CustomRecursiveTask> dividedTasks = new ArrayList<>();
        dividedTasks.add(new CustomRecursiveTask(
          Arrays.copyOfRange(arr, 0, arr.length/2)));
        dividedTasks.add(new CustomRecursiveTask(
          Arrays.copyOfRange(arr, arr.length/2, arr.length)));
        return dividedTasks;
    }

    private Integer processing(int[]arr) {
        return Arrays.stream(arr)
          .filter(a -> a > 10 && a < 27)
          .map(a -> a **  10)
          .sum();
    }
}

In diesem Beispiel wird die Arbeit durch ein Array dargestellt, das im Feld arr der Klasse CustomRecursiveTask gespeichert ist. Die createSubtask () - Methode unterteilt die Aufgabe rekursiv in kleinere Teile der Arbeit, bis jedes Stück kleiner als der Schwellenwert ist _. . Dann sendet die invokeAll () - Methode Unteraufgaben an den allgemeinen Abzug und gibt eine Liste der https://docs zurück. oracle.com/javase/8/docs/api/java/util/concurrent/Future.html[Future? _ .

Um die Ausführung auszulösen, wurde für jede Subtask die Methode join () aufgerufen.

In diesem Beispiel wird dies unter Verwendung von Java 8 Stream API ; the sum () erreicht. Die Methode wird als Darstellung der Kombination von Unterergebnissen zum Endergebnis verwendet.

4. Aufgaben an ForkJoinPool senden

Um Aufgaben an den Thread-Pool zu senden, können nur wenige Ansätze verwendet werden.

Die Methode submit () oder execute () (ihre Anwendungsfälle sind gleich)

forkJoinPool.execute(customRecursiveTask);
int result = customRecursiveTask.join();

Die Methode invoke () fragt die Task ab und wartet auf das Ergebnis und benötigt keine manuelle Verbindung:

int result = forkJoinPool.invoke(customRecursiveTask);

Die Methode invokeAll () ist die bequemste Methode, um eine Folge von ForkJoinTasks an ForkJoinPool zu übergeben. Sie nimmt Aufgaben als Parameter (zwei Aufgaben, Variablen oder eine Auflistung) und gibt ihnen eine Sammlung von Future__-Objekten zurück die Reihenfolge, in der sie hergestellt wurden.

Alternativ können Sie getrennte Methoden fork () und join () verwenden. Die Methode fork () übergibt eine Aufgabe an einen Pool, löst jedoch nicht deren Ausführung aus. Zu diesem Zweck wird die Methode join () verwendet. Im Falle von RecursiveAction gibt join () nichts außer null zurück. Für RecursiveTask <V> gibt das Ergebnis der Taskausführung zurück:

customRecursiveTaskFirst.fork();
result = customRecursiveTaskLast.join();

In unserem RecursiveTask <V> -Beispiel haben wir die Methode invokeAll () verwendet, um eine Folge von Unteraufgaben an den Pool zu senden. Dieselbe Arbeit kann mit fork () und join () ausgeführt werden, obwohl dies Folgen für die Reihenfolge der Ergebnisse hat.

Um Verwirrung zu vermeiden, empfiehlt es sich im Allgemeinen, die Methode invokeAll () zu verwenden, um mehr als eine Aufgabe an __ForkJoinPool zu übergeben.

5. Schlussfolgerungen

Die Verwendung des Fork/Join-Rahmens kann die Verarbeitung großer Aufgaben beschleunigen. Um dieses Ergebnis zu erreichen, sollten jedoch einige Richtlinien beachtet werden:

  • Verwenden Sie so wenige Thread-Pools wie möglich - in den meisten Fällen die besten

Sie entscheiden sich für einen Thread-Pool pro Anwendung oder System Verwenden Sie den Standard-Common-Thread-Pool, ** wenn keine spezielle Optimierung erforderlich ist

  • Verwenden Sie einen angemessenen Schwellenwert , um ForkJoingTask in aufzuteilen

Teilaufgaben Vermeiden Sie Blockierungen in Ihren ForkJoingTasks **

Die in diesem Artikel verwendeten Beispiele sind im https://github.com/eugenp/tutorials/tree/master/core-java-concurrency [linked GitHub-Repository verfügbar.