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:
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
.
isAvailable()
sollte nicht dadurch implementiert werden, dass geprüft wird, obresult
nochnull
ist, denn das könnte schließlich auch das legitime Ergebnis eines Ausdrucks sein.
- Stattdessen wird
finished
inrun()
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 inrun()
auch in einemsynchronized
-Block mit derselben Lock-Variable wie der Instrinsic Lock fürget()
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 dasRunnableWithResult2<T>
ausgeführt wird, in der Instanzvariablenself
gespeichert. self
mussvolatile
sein, damit von einem anderen Thread aus darauf zugegriffen werden kann (andere Möglichkeiten für Memory Barriers gehen ebenso).isAvailable()
prüft, obself
gerade noch nebenläufig abläuft (konkret dierun()
-Methode schon gestartet ist, aber noch nicht beendet ist. Ist der Threadself
fertig, ist er nicht mehr alive. Daher muss dann das Ergebnis vorliegen. Allerdings kann es sein, dassrun()
noch gar nicht angefangen hat - in dem Fall istself
aber nochnull
.
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 zuCallable<T>
(call()
statteval()
)RunnableWithResult<T>
analog zuFuture<T>
(get()
undisAvailable()
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 KlassenTaskLambda
: Lambda-AusdrückeTaskStaticClass
: 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:
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:
Lösungsmöglichkeit TaskLambda
mit Lambda-Ausdrücken:
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.