Java Threads - BS 7 Augsburg

Werbung
Java Threads
AnPr
Name
1
Klasse
Datum
Grundgedanke
Bis jetzt haben wir Programme geschrieben, welche einen Thread – den „main-Thread“ – beinhalten. Dies ist
für viele Programme auch ausreichend, da ein Programm oftmals nur das durchzuführen hat, was der User
gerade vorgibt. Die Nutzung von nur einem Thread hat aber bei manchen Problemstellungen einige Nachteile. Hier nur einige von ihnen:
 Aufwändige Rechenprozesse blockieren mitunter die Userinteraktion bzw. das Responseverhalten
unseres Programms.
 Es ist nur bedingt möglich die Multicorearchitektur der Rechner auszunutzen – vor allem bei rechenintensiven Algorithmen ist das ein großes Problem.
 Programme, welche Animationen und Userinteraktionen benötigen, können beides nur sehr schwierig umsetzen. Entweder die Animation „ruckelt“, oder die User müssen auf die Programmreaktionen
auf die Programmreaktionen warten.
Die Lösung dieser Probleme sind parallele Threads. Hierbei werden neben dem „main-Thread“ zusätzliche,
parallel laufende Threads gestartet, welche mit den zeitaufwändigen Arbeiten betraut werden. In diesem
Dokument möchte ich auf die wesentlichen Punkte von Multithread Anwendungen eingehen. Dies ist auf gar
keinen Fall eine vollständige Darstellung aller Möglichkeiten und Anforderungen. Alleine das Thema
„Thread Safety“ kann ganze Bücher füllen. Trotzdem habe ich mich bemüht, die Grundgedanken dieses
Themas zusammenzufassen. Wer mehr wissen will, kann sich in diversen Onlinehilfen schlau machen. Folgende wären ein Anfang:
http://de.wikibooks.org/wiki/Java_Standard:_Threads#Der_main-Thread
http://www.baptiste-wicht.com/series/java-concurrency-tutorial/
Weiterhin habe ich ein paar Beispielprogramme und ein Übungsprogramm erstellt, welche die Grundlagen
vermitteln sollen. Hierbei gehe ich beim Übungsprogramm auch etwas in die Spieleprogrammierung hinein,
in der Hoffnung, dass euch dir Spaß macht 
2
Der main-Thread
Wenn wir ein Java Programm starten, so wird hierbei immer ein Thread gestartet, wir nennen diesen Thread
den „main-Thread“. Er läuft (wie alle Java Threads) in der Java Virtual Machine (JVM). Im Task Manager
finden wir hier unter Windows einen Prozess namens „javaw.exe“ (für Java Programme ohne Konsole), oder
„java.exe“ (für Java Programme mit Konsole). Dieser Thread sucht nun den Einstieg in unser Programm.
Dies ist die „main“ Methode.
Nachdem der Interpeter die Main Methode als erstes abarbeitet, wird also jeglicher Code den wir schreiben,
im Kontext dieser Methode laufen. Alle weiteren Aktionen werden von hier aus angestoßen. Üblicherweise
erzeugen wir hier zuerst ein Objekt, welches sich in der Folge dann um alle weiteren Dinge kümmert.
ANPR_Threads_01.docx
Seite 1
Java Threads
3
AnPr
Der parallele Thread
Wenn wir nun einen parallelen Thread starten wollen, müssen wir folgende Punkte beachten:
 Wir brauchen Code, den der Thread ausführen soll.
 Der Thread sollte in der Lage sein, eine Methode von MyMainClass aufzurufen, um Thread-interne
Informationen an MyMainClass zu senden.
 MyMainClass muss die Möglichkeit haben aktuelle Informationen an den Thread zu übergeben und
ggf. diesen auch wieder zu stoppen.
Bevor wir uns aber um diese Themen kümmern, müssen wir uns erst einmal Gedanken darüber machen, wie
das Grundkonzept für Threads aussieht. Wir haben ja soeben gesehen, dass der „main-Thread“ über die
„main-Methode“ weiß, wo er anfangen soll. Gibt es bei parallelen Threads auch eine „main-Methode“, oder
läuft es hier anders? Folgende Grafik soll hier Licht ins Dunkel bringen:
Gehen wir den Code mal kurz durch. In „MyMainClass“ wird ein neues Thread Objekt erzeugt – was im
Wesentlichen eine Klasse darstellt, deren Konstruktor wiederum ein Objekt erwartet welches den auszuführenden Code beinhaltet. Um diesen Mechanismus zu garantieren, muss die Klasse dieses übergebenen Objektes das Interface „Runnable“ implementieren, wodurch erzwungen wird, dass die Methode „run“ implementiert wird. Diese Methode wird vom Thread aufgerufen, sobald die Methode „start“ durchlaufen wird.
Wenn wir keine weitere Referenz auf den Thread brauchen, können wir den Aufruf von „start“ gleich beim
erzeugten Objekt durchführen:
new Thread(new MyThread()).start();
Seite 2
AnPr
Java Threads
“new Thread()” erzeugt also ein Thread Objekt, das Argument “new MyThread()” erzeugt ein Objekt, welches wir definieren können. „.start()“ veranlasst Java dann, den Thread zu starten und somit in unserer Klasse „MyThread“ die „run“ Methode aufzurufen.
Soweit, so gut. Nun können wir uns um die weiter Oben geforderten Eigenschaften kümmern. Bauen wir
also unseren Code Schritt für Schritt auf:
Wie Du siehst, ist die „MyMainClass“ relativ simpel aufgebaut – unser Programm soll ja auch nur Demonstrationszwecken dienen. Beginnen wir mit den Instanzvariablen – hier halten wir eine Referenz auf ein Objekt
„myThread“, welches später den Code unseres Threads darstellen soll. Weiterhin haben wir eine (beispielhafte) Instanzvariable „myMainCounter“, welche einfach nur hochzählen wird und nach einer bestimmten
Anzahl den Thread wieder stoppen soll – wir simulieren hiermit also eine Kontrolle des Threads.
Danach kommt der Konstruktor, in dem der Thread gestartet wird. Damit er weiß, was er tun soll, erzeugen
wir unser „MyThread“ Objekt und übergeben es dem Thread. Da wir möchten, dass der Thread mit dem
Objekt der Klasse „MyMainClass“ kommuniziert (er soll einfach zyklisch eine Methode aufrufen und einen
Wert übergeben), benötigt er eine Referenz auf dieses Objekt, weshalb wir den Konstruktor von „MyThread“
mit einem Parameter vom Typ „MyMainClass“ versehen werden. Somit können wir mit dem Schlüsselwort
„this“ dem Konstruktor „MyThread“ die Referenz mitgeben.
Die zyklisch aufzurufende Methode habe ich „cyclicCalledMethod“ getauft (jaja, wer eine bessere Idee hat,
soll sie umbenennen). Der Stringparameter wird hier lediglich übernommen und auf der Konsole ausgegeben. Weiterhin zählt die Methode die Anzahl der Aufrufe mit und prüft, ob eine gewisse Grenze überschritten wurde. Wenn das der Fall ist, wird der Thread gestoppt, indem eine hierfür zu erstellende Methode
„stopMe“ aufgerufen wird.
Soviel zu der Main Klasse. Was nun fehlt ist der Thread – wobei das nicht ganz korrekt formuliert ist. Wir
erinnern uns - eigentlich ist der Thread eine von Java zur Verfügung gestellte Klasse, welche ich nur 1:1
nutze. Damit der Thread weiß, was er zu tun hat, übergebe ich ihm ein Objekt, welches das Interface
„Runnable“ implementiert und somit die Methode „run“ aufweist. Diese wiederum wird vom Thread aufgerufen und wir können unsere Funktionalität platzieren. Doch sehen wir uns dem Code dieser Klasse erst mal
an.
Seite 3
Java Threads
AnPr
Auch hier ist der Code relativ knapp gehalten. Wir kümmern uns zuerst um die Instanzvariablen. Einerseits
brauchen wir eine Referenz auf das MyMainClass Objekt, welches ja eine Instanz von „MyThread“ erzeugt
hat. Weiterhin habe ich eine Variable zur Laufkontrolle eingeführt – solange „currentlyRunning“ auf „true“
steht, soll ein zyklischer Aufruf stattfinden. Der Konstruktor übernimmt nun lediglich die Referenz auf
„MyMainClass“.
Das eigentlich spannende ist die „run“ Methode. Diese wird ja vom Thread aufgerufen, sobald ich die Methode „start()“ aufrufe. Dann soll also der Zyklus loslaufen – ergo setze ich „currentlyRunning“ auf „true“.
Nun folgt die Schleife, welche solange läuft, bis in dieser Variable wieder „false“ steht. Wie Du aber siehst,
gibt es innerhalb der Schleife keinen Code, welcher „currentlyRunning“ wieder auf „false“ setzt – in einem
singlethreaded Programm würde das im Regelfall eine Endlosschleife sein. Wir haben hier aber nun ein multithreaded Programm – insofern müssen wir damit rechnen, dass ein anderer Thread (der „main-Thread“)
diese Variable auf „false“ setzten kann (genau genommen könnte das zwar auch in einem Thread durch die
„MyMainClass“ erfolgen, aber das Prinzip sollte klar sein).
Innerhalb der Schleife wird nun lediglich eine kleine Pause gemacht (1000 Millisekunden) und danach die
Methode „cyclicCalledMethod“ von „MyMainClass“ aufgerufen. Das passiert also solange, bis irgendjemand „currentlyRunning“ auf „false“ setzt, was durch den Aufruf von „stopMe“ erfolgen würde.
Wenn wir nun den Code abtippen und ausführen sehen wir, dass wir 11 mal „Hello, I’m still running!“ sehen. Danach erfolgt die Ausgabe „I prepare to stop!“, da die while – Schleife beendet wurde und somit die
Methode „run“ endet.
Seite 4
AnPr
4
Java Threads
Thread safety
Bei multithreaded Programmen ändern sich so manche Regeln. Bspw. haben wir gesehen, dass wir durchaus
eine Schleife realisieren können, in deren Rumpf die Bedingungsvariable „currentlyRunning“ nicht direkt
verändert wird. Dies übernimmt in unserem vereinfachten Beispiel das Objekt der Klasse „MyMainClass“
und zwar durch die aufgerufene Methode „cyclicCalledMethod“, aber es sollte klar sein, dass es auch durch
einen beliebigen anderen Thread erfolgen kann.
Der wichtigste Punkt jedoch ist das Thema „Thread safety“ – also die Frage, ob ein Programm auch dann
funktioniert, wenn es durch mehrere parallele Threads genutzt und
manipuliert werden kann. Folgendes Beispiel soll dies verdeutlichen. In einem Thread (Main
Thread) existiert eine Instanzvariable. Diese wird durch eine Methode verändert (sagen wir mal,
sie wird um den Wert 1 erhöht).
Der Aufruf dieser Methode erfolgt
jedoch über einen anderen Thread
(SubThread1).
Die Methode wird also im nächsten Schritt den erhöhten Wert (hier der Wert 1) in die Instanzvariable zurückschreiben. Das ist fürs Erste
absolut problemlos und wir können auch keinerlei Fehlfunktionen
in unserem kleinen Beispiel erkennen.
Wenn wir unser Beispiel aber nun um einen weiteren Thread erweitern, dann können wir verschiedene Ablaufkonstellationen untersuchen. Im ersten (einfachen) Szenario würde der zweite Thread den Methodenaufruf dann ausführen, wenn SubThread1 den Wert der Instanzvariable bereits zurückgeschrieben hat. Insofern
würde der zweite Thread einfach aus der 1 die 2 machen. Auch das wäre problemlos. Spannender wird es
aber in der zweiten Konstellation,
wenn der zweite Thread die Methode bereits aufruft, wenn SubThread1 noch nicht fertig ist. Sehen wir uns das Beispiel rechts
mal kurz an. SubThread1 holt sich
myLoopCount mit dem Wert 0
und möchte ihn gerne auf den
Wert 1 erhöhten. Gleiches gilt für
SubThread2 – da SubThread1 den
Wert noch nicht zurückgeschrieben hat, erhält SubThread2 nun
ebenfalls den Wert 0, welche folglich auch auf den Wert 1 erhöht
wird. Wenn nun beide Threads ihr
Ergebnis zurückschreiben, werden
Seite 5
Java Threads
AnPr
beide den Wert1 zurückschreiben, was (unter Umständen) falsch sein kann. Wenn wir tatsächlich in unserem
Programm für jeden Aufruf der
Methode eine Erhöhung unseres
Wertes erwarten, so ist das hier
vorgestellte
Programm
nicht
Thread safe!
Wir müssen also schon mal feststellen, dass es für den Begriff
„Thread safety“ nicht wirklich
eine globale Definition gibt – es
kommt immer darauf an, was das
Programm machen soll. In den
meisten Fällen kann man aber
attestieren, dass wenn ein derartiger Zugriff auf Werte durch
Threads durchgeführt wird und
wir keine weiteren Maßnahmen ergreifen, es vermutlich eine Fehlfunktion darstellt.
Der zweite wichtige Punkt ist nun, dass wir solche Fehler nicht immer gleich finden können. Sie treten ja nur
in bestimmten Konstellationen auf. Um solche Situationen zu vermeiden – sie also „Thread safe“ zu machen,
gibt es die verschiedensten Möglichkeiten. Ich möchte hier auf die
einfachste eingehen – welche zugegebenermaßen für manche Situationen eher die „Dampfhammermethode“ darstellt, aber durchaus Sicherheit bietet. Wir deklarieren die Methode im Main Thread als „synchronized“. Dies bedeutet, dass die Methode nur dann aufgerufen werden
kann, wenn sie nicht gerade läuft.
Wenn wir uns den Ablauf links kurz
ansehen erkennen wir, dass der
zweite Methodenaufruf wartet, bis
der erste terminiert ist. Insofern
haben wir hier eine einfache Möglichkeit gefunden, unser Program zu „reparieren“.
Als Fazit ist festzuhalten, dass mit der multithreaded Programmierung einige neue Herausforderungen auf
uns warten. Wie wir Oben gesehen haben, können Probleme entstehen, wenn also verschiedene Threads in
zeitlich „ungüngstigen“ Konstellationen auf ein und dieselbe Methode zugreifen. Weitere mögliche Probleme können durch sogenannte „dead locks“ entstehen, wenn bspw. SubThread1 auf SubThread2 wartet, dieser jedoch wiederum auf SubThread1 wartet – das Programm würde in solchen Konstellationen schlichtweg
nicht mehr reagieren. „Race conditions“ können entstehen, wenn (ähnlich wie Oben beschrieben) die Abarbeitung zu einer zeitlichen Abfolge führt, welche in Fehlfunktionen mündet.
Lösungen für derartige Probleme sind zum Beispiel:
 „Semaphoren“ kontrollieren die Anzahl der möglichen Nutzer von Programmelementen (Code oder
Variablen)
 atomare Manipulationen sind Aktionen, welche immer komplett am Stück ausgeführt werden
 synchronized Methoden, was Methoden sind, die nie pro Objekt parallel laufen
Grundsätzlich gilt aber auch, dass sämtliche Synchronisationsmethoden sich nur über einen minimalen Bereich erstrecken dürfen, da wir sonst unnötige Wartezeiten kreieren, was wiederum die Performance beeinträchtigt.
Seite 6
AnPr
5
Java Threads
Beispielcode für thread safety
Der folgende Code soll einzig dazu dienen, das Thema thread safety zu verdeutlichen. Er ist nicht als Referenz für eigene Programme gedacht, da hier eine Provokation der thread Fehlersituation eingebaut wurde,
indem die Instanzvariable nicht direkt, sondern über eine lokale Variable manipuliert wird!
Bei diesem Beispiel werden zwei Threads gestartet, welche beide über die Methode „receiveThreadCall“
versuchen eine Instanzvariable zu ändern. Hierbei wurden diverse „Thread.sleep()“ Pausen eingefügt, um
aufwändige Berechnungen zu simulieren. Die zentrale Methode ist hierbei „receiveThreadCall“. Sie wird
sowohl von myThread1 als auch von myThread2 aufgerufen und erzeugt somit Synchronisationsprobleme.
Die Methode gibt ihre Werte über folgende Codezeile aus:
System.out.println(myLoopCount + "
" + displayString);
Es wird also der Zähler “myLoopCount” der MyMainClass ausgegeben und daneben der displayString, was
nichts anderes ist als der Threadname und ein threadinterner Zähler. Wir lassen den Code nun zweimal laufen. Einmal so, wie er unten eingetragen ist und einmal mit einer kleinen Ergänzung. Wir schreiben das Wort
„synchronized“ vor die Methode „receiveThreadCall“:
Synchronized public void receiveThreadCall(String displayString) {
Hier die Ausgaben:
Ohne “synchronized”
Mit “synchronized”
1
1
2
2
3
3
4
4
5
5
6
6
1
2
3
4
5
6
7
Thread1
Thread2
Thread1
Thread2
Thread1
Thread2
Thread1
Thread2
Thread1
Thread2
Thread1
Thread2
1
1
2
2
3
3
4
4
5
5
6
6
Thread2
Thread1
Thread2
Thread1
Thread2
Thread1
Thread2
1
1
2
2
3
3
4
Wie wir sehen, wird „myLoopCount“ links von dem zweiten Threadaufruf immer wieder überschrieben, so
dass wir in Summe 12 Durchläufe haben. Rechts haben wir in Summe 7 Durchläufe. Zuerst einmal haben wir
mit dem Schlüsselwort „synchronized“ die Methode dazu gebracht nicht „concurrent“ zu laufen. Am Schluss
läuft aber Thread1 durch und veranlasst das Stoppen beider Threads. Thread2 hat aber die Methode bereits
aufgerufen und wartet somit, bis Thread1 fertig ist. Sobald dies der Fall ist, kommt Thread2 zum Zug und
schließt seinen Befehl ab.
Hier der Code:
package threadExample;
public class MainClass {
/**
* Test for demonstrating a race condition in a multi-threaded
* situation
* @param args
*/
public static void main(String[] args) {
new MainClass();
}
Seite 7
Java Threads
AnPr
/**
* Class variable which will be manipulated by several threads.
*/
private int myLoopCount = 0;
/**
* Thread 1 which will manipulate the variable every 2 ms
*/
private MyThread myThread1 = new MyThread(this, "Thread1", 2);
/**
* Thread 2 which will manipulate the variable every 3 ms
*/
private MyThread myThread2 = new MyThread(this, "Thread2", 3);
/**
* Constructor starts the two threads
*/
public MainClass() {
// generate the threads
new Thread(myThread1).start();
new Thread(myThread2).start();
}
/**
* This method is called by the threads. It will display the value of
* the "myLoopCount" variable after manipulating it. In order to
* make the behaviour more visible, the construction of this method
* is quite "buggy". A pause time is implemented in order to simulate
* a time consuming calculation, which is set to a higher value than
* the loop time. This will lead to the race conditions.
* The method also stops the thread after a certain number of loops.
* If you want to make the method "thread save" just add the keyword
* "synchronized" before the keyword "public".
*
* @param displayString
*/
public void receiveThreadCall(String displayString) {
// Take over the class variable value in order to make the
// the "race condition"
// more transparent. If you directly manipulate the instance
// variable "myLoopCount"
// the error situation will occur not every time.
int localLoopCount = this.myLoopCount;
// stop the threads after the 5th call
if (++localLoopCount > 5) {
myThread1.stopThread();
myThread2.stopThread();
}
try {
// make a pause in order to enforce the "race condition"
Thread.sleep(100);
} catch (Exception e) {
System.out.println("Error");
}
// write back the value.
this.myLoopCount = localLoopCount;
Seite 8
AnPr
Java Threads
// print out the value
System.out.println(myLoopCount + "
" + displayString);
}
}
package threadExample;
/**
* Thread that interacts with the "MainClass" in order to demonstrate
* a race condition in a multi-threaded situation.
* @author Aicher
*/
public class MyThread implements Runnable{
/**
* Reference to the class which created the threads
*/
private MainClass callingClass = null;
/**
* As long as this value is "true", the thread will loop. To stop the
* thread just set the value to "false".
*/
private boolean threadRunning = false;
/**
* Name of the thread for easier identification of the manipulator
* of values.
*/
private String threadName = "";
/**
* Waiting time for allowing different update times of several
* class instantiations
*/
private long waitTime = 0;
/**
* Constructor sets the references to the class variables
* @param callingClass Class who instantiated this class
* @param threadName Name for identification of the thread
* @param waitTime Sleep time of the thread
*/
public MyThread (MainClass callingClass, String threadName,
long waitTime) {
this.callingClass = callingClass;
this.threadName = threadName;
this.waitTime = waitTime;
}
@Override
public void run() {
// set to true - now the thread is running. Calling
// the "stopThread()" method will stop the loop below
threadRunning = true;
// Counter for the thread loop
int loopCount = 0;
while (threadRunning) {
Seite 9
Java Threads
AnPr
try {
// wait for allowing different thread behaviours
Thread.sleep(this.waitTime);
loopCount++;
// Now call the class who created the threads for manipulating
// the counter in this class.
this.callingClass.receiveThreadCall(this.threadName + " "
+ loopCount);
} catch (Exception e) {
System.out.println("Error");
}
}
}
/**
* Call this method in order to stop the thread properly
*/
public void stopThread() {
threadRunning = false;
}
}
Seite 10
AnPr
6
Java Threads
Lizenz
Diese(s) Werk bzw. Inhalt von Maik Aicher (www.codeconcert.de) steht unter einer
Creative Commons Namensnennung - Nicht-kommerziell - Weitergabe unter gleichen Bedingungen 3.0 Unported Lizenz.
Seite 11
Herunterladen