Musterlösungen

Laboraufgabe “Thread für Runnable mit Rückgabewert”

RunnableWithResult<T>

Die Methode run() in RunnableWithResult muss überschrieben werden. In der Methode muss der Ausdruck ausgewertet werden. Das Ergebnis sollte in einer Instanzvariablen gespeichert werden. Um den Inhalt der Instanzvariablen aus einem aufrufenden Thread auslesen zu können, wird ein Getter benötigt.

Sollte die Auswertung des Ausdrucks länger benötigen, muss der Aufrufer feststellen können, ob das Ergebnis schon vorliegt. Dafür ist die Methode isAvailable() vorgesehen. Getter und isAvailable() sollen beide asynchron arbeiten: Egal, ob ein Wert vorliegt oder nicht, der Aufruf blockiert nicht.

Im Folgenden werden zwei Lösungsmöglichkeiten vorgestellt. Bei beiden Lösungsmöglichkeiten wird das Resultat der Berechnung in einer privaten Variable vom Typ T gespeichert und es gibt eine Getter-Methode, mit der dieser Wert ausgelesen werden kann:

private T result;

@Override
public synchronized T get() {
    return this.result;
}

Lösungsmöglichkeit 1 (umgesetzt in RunnableWithResult1)

Die Idee an dieser Lösung ist, dass neben dem Resultat auch die Information über die Beendigung in privaten Instanzvariablen gespeichert wird (finished).

Der Typ von finished ist boolean.

private boolean finished;
  • isAvailable() sollte nicht dadurch implementiert werden, dass geprüft wird, ob result noch null ist, denn das könnte schließlich auch das legitime Ergebnis eines Ausdrucks sein.
@Override
public synchronized Boolean isAvailable() {
  return this.finished;
}
  • Stattdessen wird finished in run() gesetzt, wenn der Ausdruck fertig berechnet wurde.
@Override
public void run() {
    synchronized (this) { // Memorybarrier
        this.finished = false;
    }
    this.result = expr().eval();
    synchronized (this) {
        this.finished = true;
    } // Memorybarrier
  • Durch den synchronized-Zugriff liegt jeder Zugriff auf eine Instanzvariable hinter einer Memory-Barrier.
  • Den Zugriff auf get() könnte man leicht blockierend machen, wenn die Berechnung in run() auch in einem synchronized-Block mit derselben Lock-Variable wie der Instrinsic Lock für get() ablaufen würde.

Lösungsmöglichkeit 2 (umgesetzt in RunnableWithResult2)

  • Result wird in wie bei Lösungsmöglichkeit 1 privater Instanzvariablen gespeichert (result)
  • Zum Beginn von run() wird der aktuelle Thread, also der Thread, in dem das RunnableWithResult2<T> ausgeführt wird, in der Instanzvariablen self gespeichert.
  • self muss volatile sein, damit von einem anderen Thread aus darauf zugegriffen werden kann (andere Möglichkeiten für Memory Barriers gehen ebenso).
  • isAvailable() prüft, ob self gerade noch nebenläufig abläuft (konkret die run()-Methode schon gestartet ist, aber noch nicht beendet ist. Ist der Thread self fertig, ist er nicht mehr alive. Daher muss dann das Ergebnis vorliegen. Allerdings kann es sein, dass run() noch gar nicht angefangen hat - in dem Fall ist self aber noch null.
private volatile Thread self;

@Override
public void run() {
    this.self = Thread.currentThread();
    this.result = expr().eval();
}

@Override
public synchronized Boolean isAvailable() {
    return (this.self != null) && !this.self.isAlive();
}

Main

Programmieren Sie in der vorbereiteten Klasse Main in der Methode main einen beispeilhaften Aufruf: Erzeugen Sie dazu drei RunnableWithResult-Objekte für die Berechnung des Ausdrucks \(((1+2)+(3+4))\), die nebenläufig zum Main-Thread ausgeführt werden. Das Ergebnis soll ausgegeben werden. Sie können Lambda-Ausdrücke* oder anonyme innere Klassen benutzen um die Expression’s zu erzeugen.*

public static void main(String... args) throws InterruptedException {
    var r1 = new RunnableWithResult2<>(() -> {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return 1 + 2;
    });
    var r2 = new RunnableWithResult2<>(() -> 3 + 4);
    var r3 = new RunnableWithResult2<>(() -> {
        while (!r1.isAvailable() || !r2.isAvailable()) {
            System.out.println("waiting on r1 or r2");
        }
        return r1.get() + r2.get();
    });

    var thread1 = new Thread(r1);
    var thread2 = new Thread(r2);
    var thread3 = new Thread(r3);

    thread1.start();
    thread2.start();
    thread3.start();

    while (!r3.isAvailable()) {
        System.out.println("waiting on r3");
    }
    System.out.println("result: " + r3.get());
}

In diesem Beispiel wird in \(r1\) vor dem Berechnen und der Rückgabe von \(1+2\) zuerst 3 Sekunden gewartet, damit die Teilaufgaben unterschiedlich lange in der Berechnung brauchen.

Exceptions

Achtung: Der Umgang mit Exceptions bei RunnableWithResult<T>, die während der Auswertung des Ausdrucks auftreten könnten, ist in dieser Umsetzung nicht sehr elaboriert. Probieren Sie es aus, indem Sie einen “Division durch 0 Error” in einem RunnableWithResult<>-Ausdruck provozieren. Wann wird die Exception realisiert?

Eine Exception wird sofort geworfen, wenn sie passiert. Das ist ungünstig, weil die Fehersuche schwierig, wenn der Programmfluss dadurch asynchron unterbrochen wird. Außerdem ist ungünstig, dass die Behandlungsroutine sich nur in der Expression befinden kann.

Laboraufgabe “asynchrone Ausführung mit Callable und Future

Quellcode von Main

public class Main {

    public static void main(String... args) {
        var executor = Executors.newCachedThreadPool();
        // Lambda-Ausdruck, mehrere Statements, explizites return
        var f1 = executor.submit(() -> {
            return 1.0 + 2.0;
        });
        // Callable als Inner Class
        var f2 = executor.submit(new Callable<Double>() {
            @Override
            public Double call() throws Exception {
                return 3.0 + 4.0;
            }
        });
        // Lambda-Ausdruck, knapp
        var f3 = executor.submit(() -> f1.get() + f2.get());
        try {
            // get() blockiert, bis etwas vorliegt (auch oben)
            System.out.println(f3.get());
        } catch (InterruptedException | ExecutionException e) {
            // Exceptions in f1 und f2 werden bis zum f3.get() verzögert
            Thread.currentThread().interrupt();
        } finally {
            executor.shutdown();
        }
    }
}

Callable<T> und Future<T>

  • Expression<T> analog zu Callable<T> (call() statt eval())
  • RunnableWithResult<T> analog zu Future<T> (get() und isAvailable() wie !isDone())

Exceptions

Die Exception wird erst beim Zugriff mit get() realisert. Eine Exeption in einem Callable eines Future-Threads wird erst beim Zugriff mit get() an den Aufrufer weitergereicht. Dort muss sie behandelt werden, nicht in der call()-Methode des Callable’s.

Laboraufgabe “Thread Pools”

Die folgenden Klassen zeigen Lösungsskizzen mit unterschiedlichen Methoden:

  • TaskInnerClass: anonyme innere Klassen
  • TaskLambda: Lambda-Ausdrücke
  • TaskStaticClass: statische Klassen

Größe und Verhalten der Thread Pools

Sie gleichen sich darin, dass der Name des Threads ausgegben wird, der gerade den Task im (noch unbekannten) Thread Pool ausführt. Aus der Wiederverwendung von Namen können Rückschlüsse auf Größe und Verhalten des Thread Pools gezogen werden, die jeweils von pp.FixedRunner.test und pp.CachedRunner.test verwendet werden.

Die Größe des Fixed Thread Pools ist 6. Die maximale Größe des Cached Thread Pools hängt von Ihrem Rechner und vom Timing der Tasks ab.

Lösungsmöglichkeit TaskInnerClass

mit anonymen inneren Klassen:

var pool1 = FixedRunner.test(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}, NUMBER_OF_TASKS);
var pool2 = CachedRunner.test(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}, NUMBER_OF_TASKS);

Lösungsmöglichkeit TaskLambda

mit Lambda-Ausdrücken:

var pool1 = FixedRunner.test(
        () -> System.out.println(Thread.currentThread().getName()),
        NUMBER_OF_TASKS);
var pool2 = CachedRunner.test(
        () -> System.out.println(Thread.currentThread().getName()),
        NUMBER_OF_TASKS);

Lösungsmöglichkeit TaskStaticClass

mit statischen Klassen:

public class TaskStaticClass implements Runnable {

    /* ... */

    private static final int NUMBER_OF_TASKS = 20;
    private static ExecutorService pool1;
    private static ExecutorService pool2;
    private static ScheduledExecutorService scheduler;

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String... args) {
        pool1 = FixedRunner.test(new TaskStaticClass(),
                NUMBER_OF_TASKS);
        pool2 = CachedRunner.test(new TaskStaticClass(),
                NUMBER_OF_TASKS);

    /* ... */
}

Herunterfahren des Thread Pools am Ende

Am Ende wird ein ScheduledExecutorService namens scheduler benutzt um nach einer Verzögerung von 5 Sekunden beide Thread Pools mit shutdown() herunterzufahren. Ohne dies bleiben die Thread Pools aktiv.

Dies sieht bei anonymen inneren Klassen, Lambda-Ausdrücken und statischen Klassen etwas unterschiedlich aus:

Lösungsmöglichkeit TaskInnerClass

mit anonymen inneren Klassen:

var scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.schedule(new Runnable() {
    @Override
    public void run() {
        pool1.shutdown();
        pool2.shutdown();
        scheduler.shutdown();
    }
}, 5, TimeUnit.SECONDS);

Lösungsmöglichkeit TaskLambda

mit Lambda-Ausdrücken:

var scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.schedule(() -> {
    pool1.shutdown();
    pool2.shutdown();
    scheduler.shutdown();
}, 5, TimeUnit.SECONDS);

Lösungsmöglichkeit TaskStaticClass

mit statischen Klassen:

static class Shutdowner implements Runnable {
    @Override
    public void run() {
        pool1.shutdown();
        pool2.shutdown();
        scheduler.shutdown();
    }
}

private static ScheduledExecutorService scheduler;

public static void main(String... args) {
    /* ... */
    scheduler = Executors.newSingleThreadScheduledExecutor();
    scheduler.schedule(new Shutdowner(), 5, TimeUnit.SECONDS);
}

Bewertung

Zusammenfassend kann man festhalten, dass Lambda-Ausdrücke im Vergleich zu statischen Klassen und anonymen innneren Klassen von der Menge des benötigten Quellcodes am kompaktesten sind und am wenigsten Wissen über erforderliche Typen (Runnable, Callable etc.) erforderlich ist.