Überblick über die java.util.concurrent

Überblick über das java.util.concurrent

1. Überblick

Das Paketjava.util.concurrententhält Tools zum Erstellen gleichzeitiger Anwendungen.

In diesem Artikel geben wir einen Überblick über das gesamte Paket.

2. Hauptbestandteile

java.util.concurrent enthält viel zu viele Funktionen, um sie in einem einzigen Bericht zu diskutieren. In diesem Artikel konzentrieren wir uns hauptsächlich auf einige der nützlichsten Hilfsprogramme aus diesem Paket wie:

  • Testamentsvollstrecker

  • ExecutorService

  • ScheduledExecutorService

  • Zukunft

  • CountDownLatch

  • CyclicBarrier

  • Semaphor

  • ThreadFactory

  • BlockingQueue

  • DelayQueue

  • Schlösser

  • Phaser

Hier finden Sie auch viele Artikel zu den einzelnen Klassen.

2.1. Executor

Executor ist eine Schnittstelle, die ein Objekt darstellt, das bereitgestellte Aufgaben ausführt.

Es hängt von der jeweiligen Implementierung (von der aus der Aufruf initiiert wird) ab, ob die Task auf einem neuen oder aktuellen Thread ausgeführt werden soll. Mit dieser Schnittstelle können wir also den Task-Ausführungsfluss vom eigentlichen Task-Ausführungsmechanismus entkoppeln.

Hierbei ist zu beachten, dassExecutor nicht unbedingt eine asynchrone Taskausführung erfordert. Im einfachsten Fall kann ein Executor die übergebene Aufgabe sofort im aufrufenden Thread aufrufen.

Wir müssen einen Aufrufer erstellen, um die Executor-Instanz zu erstellen:

public class Invoker implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

Jetzt können wir diesen Aufrufer verwenden, um die Aufgabe auszuführen.

public void execute() {
    Executor executor = new Invoker();
    executor.execute( () -> {
        // task to be performed
    });
}

Hierbei ist zu beachten, dass der ExecutorRejectedExecutionException auslöst, wenn er die Aufgabe nicht zur Ausführung annehmen kann.

2.2. ExecutorService

ExecutorService ist eine Komplettlösung für die asynchrone Verarbeitung. Es verwaltet eine speicherinterne Warteschlange und plant übermittelte Aufgaben basierend auf der Thread-Verfügbarkeit.

UmExecutorService, zu verwenden, müssen wir eineRunnable-Klasse erstellen.

public class Task implements Runnable {
    @Override
    public void run() {
        // task details
    }
}

Jetzt können wir die Instanz vonExecutorServiceerstellen und diese Aufgabe zuweisen. Zum Zeitpunkt der Erstellung müssen wir die Thread-Pool-Größe angeben.

ExecutorService executor = Executors.newFixedThreadPool(10);

Wenn wir eine Single-Threaded-InstanzExecutorServiceerstellen möchten, können wirnewSingleThreadExecutor(ThreadFactory threadFactory) verwenden, um die Instanz zu erstellen.

Sobald der Executor erstellt ist, können wir ihn zum Senden der Aufgabe verwenden.

public void execute() {
    executor.submit(new Task());
}

Wir können auch die Instanz vonRunnableerstellen, während wir die Aufgabe senden.

executor.submit(() -> {
    new Task();
});

Außerdem werden zwei sofort einsatzbereite Methoden zur Beendigung der Ausführung mitgeliefert. Der erste istshutdown(); Es wartet, bis die Ausführung aller übergebenen Aufgaben abgeschlossen ist. Die andere Methode istshutdownNow(), wobeih alle anstehenden / ausgeführten Aufgaben sofort beendet.

Es gibt auch eine andere MethodeawaitTermination(long timeout, TimeUnit unit), die zwangsweise blockiert, bis alle Aufgaben ausgeführt wurden, nachdem ein Abschaltereignis ausgelöst oder ein Ausführungszeitlimit aufgetreten ist oder der Ausführungsthread selbst unterbrochen wurde.

try {
    executor.awaitTermination( 20l, TimeUnit.NANOSECONDS );
} catch (InterruptedException e) {
    e.printStackTrace();
}

2.3. ScheduledExecutorService

ScheduledExecutorService ist eine ähnliche Schnittstelle wieExecutorService,, kann jedoch regelmäßig Aufgaben ausführen.

Executor and ExecutorService‘s methods are scheduled on the spot without introducing any artificial delay. Null oder ein negativer Wert bedeutet, dass die Anforderung sofort ausgeführt werden muss.

Wir können sowohl dieRunnable- als auch dieCallable-Schnittstelle verwenden, um die Aufgabe zu definieren.

public void execute() {
    ScheduledExecutorService executorService
      = Executors.newSingleThreadScheduledExecutor();

    Future future = executorService.schedule(() -> {
        // ...
        return "Hello world";
    }, 1, TimeUnit.SECONDS);

    ScheduledFuture scheduledFuture = executorService.schedule(() -> {
        // ...
    }, 1, TimeUnit.SECONDS);

    executorService.shutdown();
}

ScheduledExecutorService kann auch die Aufgabeafter some given fixed delay planen:

executorService.scheduleAtFixedRate(() -> {
    // ...
}, 1, 10, TimeUnit.SECONDS);

executorService.scheduleWithFixedDelay(() -> {
    // ...
}, 1, 10, TimeUnit.SECONDS);

Hier erstellt und führt die MethodescheduleAtFixedRate( Runnable command, long initialDelay, long period, TimeUnit unit )eine periodische Aktion aus, die zuerst nach der angegebenen anfänglichen Verzögerung und anschließend mit der angegebenen Zeitspanne bis zum Herunterfahren der Dienstinstanz aufgerufen wird.

Die MethodescheduleWithFixedDelay( Runnable command, long initialDelay, long delay, TimeUnit unit ) erstellt eine periodische Aktion und führt sie aus, die zuerst nach der angegebenen anfänglichen Verzögerung und wiederholt mit der angegebenen Verzögerung zwischen der Beendigung der ausführenden und dem Aufruf der nächsten aufgerufen wird.

2.4. Future

Future is used to represent the result of an asynchronous operation. Es enthält Methoden zum Überprüfen, ob der asynchrone Vorgang abgeschlossen ist oder nicht, zum Abrufen des berechneten Ergebnisses usw.

Darüber hinaus bricht diecancel(boolean mayInterruptIfRunning)-API die Operation ab und gibt den ausführenden Thread frei. Wenn der Wert vonmayInterruptIfRunning wahr ist, wird der Thread, der die Aufgabe ausführt, sofort beendet.

Andernfalls können laufende Aufgaben ausgeführt werden.

Wir können den folgenden Codeausschnitt verwenden, um eine zukünftige Instanz zu erstellen:

public void invoke() {
    ExecutorService executorService = Executors.newFixedThreadPool(10);

    Future future = executorService.submit(() -> {
        // ...
        Thread.sleep(10000l);
        return "Hello world";
    });
}

Mit dem folgenden Code-Snippet können wir überprüfen, ob das zukünftige Ergebnis bereit ist, und die Daten abrufen, wenn die Berechnung abgeschlossen ist:

if (future.isDone() && !future.isCancelled()) {
    try {
        str = future.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

Wir können auch eine Zeitüberschreitung für eine bestimmte Operation angeben. Wenn die Aufgabe länger als diese Zeit dauert, wird einTimeoutException ausgelöst:

try {
    future.get(10, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
    e.printStackTrace();
}

2.5. CountDownLatch

CountDownLatch (eingeführt inJDK 5) ist eine Dienstprogrammklasse, die eine Reihe von Threads blockiert, bis eine Operation abgeschlossen ist.

ACountDownLatch wird mit einemcounter(Integer-Typ initialisiert); Dieser Zähler wird dekrementiert, wenn die Ausführung der abhängigen Threads abgeschlossen ist. Sobald der Zähler jedoch Null erreicht, werden andere Threads freigegeben.

Sie können mehr überCountDownLatchhere erfahren.

2.6. CyclicBarrier

CyclicBarrier funktioniert fast genauso wieCountDownLatch, außer dass wir es wiederverwenden können. Im Gegensatz zuCountDownLatch können mehrere Threads mithilfe derawait()-Methode (als Barrierebedingung bezeichnet) aufeinander warten, bevor die letzte Aufgabe aufgerufen wird.

Wir müssen eineRunnable Task-Instanz erstellen, um die Barrierebedingung zu initiieren:

public class Task implements Runnable {

    private CyclicBarrier barrier;

    public Task(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    @Override
    public void run() {
        try {
            LOG.info(Thread.currentThread().getName() +
              " is waiting");
            barrier.await();
            LOG.info(Thread.currentThread().getName() +
              " is released");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

}

Jetzt können wir einige Threads aufrufen, um um die Barrierebedingung zu rennen:

public void start() {

    CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
        // ...
        LOG.info("All previous tasks are completed");
    });

    Thread t1 = new Thread(new Task(cyclicBarrier), "T1");
    Thread t2 = new Thread(new Task(cyclicBarrier), "T2");
    Thread t3 = new Thread(new Task(cyclicBarrier), "T3");

    if (!cyclicBarrier.isBroken()) {
        t1.start();
        t2.start();
        t3.start();
    }
}

Hier prüft die MethodeisBroken(), ob einer der Threads während der Ausführungszeit unterbrochen wurde. Wir sollten diese Prüfung immer vor dem eigentlichen Vorgang durchführen.

2.7. Semaphore

DasSemaphore wird verwendet, um den Zugriff auf Thread-Ebene auf einen Teil der physischen oder logischen Ressource zu blockieren. Ein Semaphor enthält eine Reihe von Genehmigungen. Wenn ein Thread versucht, in den kritischen Abschnitt zu gelangen, muss er das Semaphor überprüfen, ob eine Genehmigung verfügbar ist oder nicht.

Wenn keine Genehmigung verfügbar ist (übertryAcquire()), darf der Thread nicht in den kritischen Abschnitt springen. Wenn die Genehmigung jedoch verfügbar ist, wird der Zugriff gewährt und der Genehmigungszähler verringert sich.

Sobald der ausführende Thread den kritischen Abschnitt freigibt, erhöht sich der Zulassungszähler erneut (erfolgt nach der Methoderelease()).

Mit der MethodetryAcquire(long timeout, TimeUnit unit)können wir ein Zeitlimit für den Zugriff festlegen.

Wir können auch die Anzahl der verfügbaren Genehmigungen oder die Anzahl der Threads überprüfen, die darauf warten, das Semaphor zu erhalten.

Das folgende Code-Snippet kann verwendet werden, um ein Semaphor zu implementieren:

static Semaphore semaphore = new Semaphore(10);

public void execute() throws InterruptedException {

    LOG.info("Available permit : " + semaphore.availablePermits());
    LOG.info("Number of threads waiting to acquire: " +
      semaphore.getQueueLength());

    if (semaphore.tryAcquire()) {
        try {
            // ...
        }
        finally {
            semaphore.release();
        }
    }

}

Wir können eineMutex-ähnliche Datenstruktur mitSemaphore implementieren. Weitere Details zu diesemcan be found here.

2.8. ThreadFactory

Wie der Name schon sagt, fungiertThreadFactory als Thread-Pool (nicht vorhanden), der bei Bedarf einen neuen Thread erstellt. Die Implementierung effizienter Thread-Erstellungsmechanismen erfordert nicht mehr viel Boilerplate-Codierung.

Wir können einThreadFactory definieren:

public class exampleThreadFactory implements ThreadFactory {
    private int threadId;
    private String name;

    public exampleThreadFactory(String name) {
        threadId = 1;
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, name + "-Thread_" + threadId);
        LOG.info("created new thread with id : " + threadId +
            " and name : " + t.getName());
        threadId++;
        return t;
    }
}

Wir können diesenewThread(Runnable r)-Methode verwenden, um zur Laufzeit einen neuen Thread zu erstellen:

exampleThreadFactory factory = new exampleThreadFactory(
    "exampleThreadFactory");
for (int i = 0; i < 10; i++) {
    Thread t = factory.newThread(new Task());
    t.start();
}

2.9. BlockingQueue

Bei der asynchronen Programmierung istproducer-consumer pattern eines der häufigsten Integrationsmuster. Dasjava.util.concurrent-Paket enthält eine Datenstruktur namensBlockingQueue - was in diesen asynchronen Szenarien sehr nützlich sein kann.

Weitere Informationen und ein funktionierendes Beispiel hierzu finden Sie unterhere.

2.10. DelayQueue

DelayQueue ist eine unendliche Blockierungswarteschlange von Elementen, in die ein Element nur gezogen werden kann, wenn seine Ablaufzeit (als benutzerdefinierte Verzögerung bezeichnet) abgelaufen ist. Daher hat das oberste Element (head) die größte Verzögerung und wird zuletzt abgefragt.

Weitere Informationen und ein funktionierendes Beispiel hierzu finden Sie unterhere.

2.11. Locks

Es überrascht nicht, dassLock ein Dienstprogramm ist, mit dem andere Threads daran gehindert werden, auf ein bestimmtes Codesegment zuzugreifen, abgesehen von dem Thread, der es gerade ausführt.

Der Hauptunterschied zwischen einer Sperre und einem synchronisierten Block besteht darin, dass der synchronisierte Block vollständig in einer Methode enthalten ist. Wir können jedoch den lock () - und den unlock () - Vorgang der Lock-API in getrennten Methoden ausführen.

Weitere Informationen und ein funktionierendes Beispiel hierzu finden Sie unterhere.

2.12. Phaser

Phaser ist eine flexiblere Lösung alsCyclicBarrier undCountDownLatch - wird als wiederverwendbare Barriere verwendet, auf die die dynamische Anzahl von Threads warten muss, bevor die Ausführung fortgesetzt wird. Wir können mehrere Ausführungsphasen koordinieren und für jede Programmphase einePhaser-Instanz wiederverwenden.

Weitere Informationen und ein funktionierendes Beispiel hierzu finden Sie unterhere.

3. Fazit

In diesem allgemeinen Übersichtsartikel haben wir uns auf die verschiedenen Dienstprogramme konzentriert, die für das Paketjava.util.concurrentverfügbar sind.

Wie immer ist der vollständige Quellcodeover on GitHub verfügbar.