Musterlösungen
Laboraufgabe “Start eines Threads durch und bei Vererbung”
Die folgende Musterlösung gehört zum Projekt pp.01.01-Inheritance
. Der vollständige Quellcode der Musterlösung ist im Projekt pp.01.01-Inheritance_solution
:
- Der vorliegende Quellcode ist nur teilweise funktionsfähig. Probieren Sie die
main
-Methoden vonSensor
undThermometer
aus.Sensor
läuft,Thermometer
nicht.
- Analysieren Sie den Programmablauf.
- Der Konstruktor von
Thermometer
ruft zuerst den Konstruktor vonSensor
auf. Am Ende desSensor
-Konstruktors wirdstart()
aufgerufen. DerThermometer
-Thread startet also ohne, dass die Initialisierung beendet wurde:rand
ist deshalb beim ersten Aufruf vonnextInt(...)
null
.
- Der Konstruktor von
- Korrigieren Sie das Programm, so dass beide Klassen lauffähig sind.
Sensor
:start()
von Konstruktor inmain
-Methode verschieben.Thermometer
:start()
für beide Threads inmain
-Methode aufnehmen.
- Verallgemeinern Sie Ihre “Findings” zu einem kurzen Merksatz!
- Niemals
start()
in Konstruktor einer Threadsubklasse, denn das erschwert die Wiederverwendung durch Vererbung.
- Niemals
Laboraufgabe “Start eines Threads als Runnable
und Refactoring”
Die folgende Musterlösung gehört zum Projekt pp.01.02-Runnable
. Der vollständige Quellcode der Musterlösung ist im Projekt pp.01.02-Runnable_solution
:
- Lassen Sie
Starter
laufen und analysieren Sie das Verhalten. Ändern Sie denStarter
so, dass stattMyWorker
Instanzen vonMyWorkerCoop
erzeugt werden.
public static void main(String... args) {
for (var i = 0; i < Starter.WORKERS; i++) {
var t = new Thread(new MyWorkerCoop(), String.format("Worker-%03d", i));
t.start();
}
}
- Vergleichen Sie das Verhalten von beiden Varianten. Recherchieren Sie die API-Beschreibung von
Thread.yield()
und erklären Sie sich den Unterschied. - Mit
yield()
wird dem Scheduler signalisiert, dass beim Pseudo-Multitasking ein Wechsel zum nächsten wartenden Thread möglich ist. Deryield()
aufrufende Thread gibt die Ressourcen zu diesem Punkt aktiv an den Scheduler zurück. Allerdings müssen JVM-Scheduler-Implementierungen dies nicht berücksichtigen (vgl. Java-API1). Falls doch, wird es dadurch öfter zu Kontextwechseln kommen. - Erzeugen Sie eine Kopie der Klasse
Starter
namensStarterInner
, die dieselbe Funktion hat wieStarter
aber anstatt einer externen Klasse für den Worker eine anonyme innere Klasse verwendet.
package pp;
public class StarterInner {
static int WORKERS = 200;
public static void main(String... args) {
for (var i = 0; i < StarterInner.WORKERS; i++) {
var t = new Thread(new Runnable() {
private Thread self;
@Override
public void run() {
this.self = Thread.currentThread();
while (true) {
System.out.println(this.self.getName() + ": ID => " + this.self.threadId());
}
}
}, String.format("Worker-%03d", i));
t.start();
}
}
}
- Erzeugen Sie eine Kopie der Klasse
Starter
namensStarterLambda
, die dieselbe Funktion hat wieStarter
aber anstatt einer externen Klasse für den Worker einen Lambda-Ausdruck verwendet.- Die Instanzvariable
self
muss zu einer lokalen Variable inrun()
gemacht werden. Inhaltlich ist das in diesem Fall kein Problem.
- Die Instanzvariable
package pp;
public class StarterLambda {
static int WORKERS = 200;
public static void main(String... args) {
for (var i = 0; i < StarterLambda.WORKERS; i++) {
var t = new Thread(() -> {
var self = Thread.currentThread();
while (true) {
System.out.println(self.getName() + ": ID => " + self.threadId());
}
}, String.format("Worker-%03d", i));
t.start();
}
}
}
Laboraufgabe “Lebenszyklus von Threads und Interrupts”
Analysieren Sie
Task
. Wie lange wird mitThread.sleep()
gewartet, bis es zurException
kommt?- 1000ms/10 + 1000ms/9 + 1000ms/8 + … 1000ms/1
- (100+111+125+143+167+200+250+333+500+1000)ms
- 2929ms
Analysieren Sie den Quellcode von
Runner
und lassen SieRunner
laufen. Es sollte eine Exception auftreten. Wo wird sie behandelt?- im Rumpf des Lambda-Ausdrucks, der bei
UncaughtExceptionHandler
inRunner
übergeben wird. Alternativ wäre es an dieser Stelle auch möglich, eine anonyme innere Klasse zu instantiieren:
thread.setUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.err.println("Unhandled Exception: " + e.getMessage()); System.err.println(" Thread: " + t.threadId() + " - " + t.getName()); System.err.println(" Thread State: " + t.getState()); e.printStackTrace(System.err); } });
- im Rumpf des Lambda-Ausdrucks, der bei
Ändern Sie
Runner
so, dass nach dem Start vontask
ein weiterer Thread erzeugt und gestartet wird, der dieTask
-Instanztask
von außen beendet.Tipp: Sparen Sie unnötige Tipparbeit und verwenden Sie einen Lambda-Ausdruck, um das Functional Interface (
run()
) vonRunnable
zu füllen, den Sie einfach einer neuenThread
-Instanz übergeben können, die Sie sogleich starten (ein “Einzeiler”).Es sollte eine Exception auftreten. Wo wird sie behandelt?
- im
catch
-Block in derrun()
-Methode inTask
:
- im
try {
Thread.sleep(1000 / i--);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
weil this.self.interrupt())
in stopRequest()
aufgerufen wird, während Thread.sleep(1000 / i--)
ausgeführt wird.
Warum ist es erforderlich in
stopRequest()
vonTask
dem Thread vonTask
, dessen Referenz inself
gespeichert ist, mitinterrupt()
ein Signal zu schicken, ob wohl in die Hauptschleife vonrun()
doch dadurch abgebrochen wird?- sonst wird ein lange laufendes
Thread.sleep(...)
nicht unterbrochen, sondern zu Ende geführt.
- sonst wird ein lange laufendes
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Thread.html#yield() (letzter Zugriff: 04.10.2024)↩︎