Einführung in Konzept und Anwendung

Threads in Java

Mike Werner

In der heutigen Leistungsgesellschaft muss der Mensch fast schon selbst zum Computer mutieren, will er die Aufgaben, die an ihn gestellt werden, alle gleichzeitig erledigen. Doch unterscheidet sich der Mensch vom Computer durch seinen Verstand. Dieser ist dann auch der Grund, weshalb schon frühzeitig (Betriebs-) Systeme geschaffen wurden, welche auf Parallel-Verarbeitung basieren, um die steigende Datenflut zu bewältigen.

Entgegen dem ersten Eindruck befasst sich dieser Artikel allerdings nicht mit psycho-sozialen Auswirkungen unserer Gesellschaft auf den Menschen, sondern der Parallel-Verarbeitung in Java. Jeder seriöse Java-Entwickler sollte mit dem Konzept und dessen Anwendung vertraut sein. Deshalb richtet sich dieser Artikel nicht nur an den Java-Neuling, sondern soll auch dem erfahrenen Entwickler zur Auffrischung, ggf. Ergänzung seiner Kenntnisse dienen.

Allgemeiner Hinweis

Die Java Thread-API weist mehrere veraltete Teile (Deprecated) auf. Dieser Artikel berücksichtigt diese nicht, da derartige Methoden nicht mehr verwendet werden sollten. Das hat wiederum Auswirkungen auf den Inhalt des Artikels. Deshalb sei hier ausdrücklich darauf hingewiesen, dass Sachverhalte unter Umständen anders dargestellt werden als vor der Aufhebung der Gültigkeit dieser Methoden.

Wer schon mal eine Anwendung mit Benutzeroberfläche für ein Single-Tasking-Betriebssystem (z.B. MS DOS) entwickelt hat, weiß, dass auch die Programmierung der GUI, eine entsprechende API vorausgesetzt, sehr simple war. Im Allgemeinen wurden potentielle Ereignisquellen (z.B. Tastatur, Maus) in einer Schleife abgeklappert, um auf Benutzereingaben zu reagieren. Bei anspruchsvolleren Anwendungen stieß man aber recht schnell auf Probleme, welche eher schlecht als recht gelöst werden konnten – nämlich dann, wenn zusätzlich zur Benutzerinteraktion fortlaufende Aktionen auszuführen waren – z.B. bei industriellen Anwendungen wie Überwachung von Maschinen. Beim Single-Tasking kann zu einem Zeitpunkt nur eine Aufgabe ausgeführt werden, deshalb müssen Verarbeitung der Benutzereingabe und Kommunikation mit externen Gerätschaften sequentiell erfolgen. Dies hat wiederum zur Folge, dass bei der Verarbeitung der Benutzereingabe unter Umständen wichtige Daten verloren gehen. Weiterhin wird kostbare Rechenzeit verschwendet, da die Ausführung eines einzelnen Programms den Rechner nur äußerst selten voll auslastet. Aufgrund dessen wurden schon sehr früh Multi-Tasking-Betriebssysteme entwickelt (z.B. Unix), auch in Java wurde natürlich nicht auf dieses wertvolle Feature verzichtet. Die Multi-Tasking-Unterstützung, in Form von Threads, ist zum einen in der Laufzeitumgebung (JVM) und zum anderen in der Sprache selbst implementiert.

Threads ermöglichen die parallele Ausführung von Programmen bzw. Programmteilen. Dabei werden diese, sofern es sich nicht um ein Multi-Prozessor-System handelt, natürlich nicht wirklich gleichzeitig ausgeführt. Geschicktes Umschalten der JVM zwischen den einzelnen Ausführungseinheiten erweckt lediglich den Anschein als würden diese parallel arbeiten. Selbst das für jeden Einsteiger obligatorische Hello World !!!-Programm muss schon mit anderen Threads um Systemressourcen kämpfen. Denn die JVM verwaltet zum einen im Hintergrund System-Threads, welche z.B. Eingaben verarbeiten oder nicht mehr benötigten Speicher freigeben (Garbage Collector), zum anderen wird ein separater Thread erzeugt, welcher das eigentliche Programm ausführt.

In Java wird ein Thread von der Klasse java.lang.Thread repräsentiert. Um ihn zu starten, genügt es seine start-Methode aufzurufen. Dabei wird dem Thread Scheduler mitgeteilt, dass dieser bereit zur Ausführung ist. Aufgrund der wenig restriktiven Spezifikation kann der Scheduler Teil der JVM sein, aber auch Teil des Betriebssystems. Seine Aufgabe ist zu bestimmen, welchem Thread die CPU zugeteilt wird. Aber was führt ein Thread eigentlich aus?

public class Counter implements Runnable{
public void run(){
for(int i = 1; i 

Jedem Thread ist eine Priorität zugeteilt. Dabei handelt es sich um einen integer-Wert von 1 bis 10, wobei 1 die niedrigste und 10 die höchste Priorität darstellt. Die Priorität wird vom Scheduler dazu verwendet zu entscheiden, welchem bereiten Thread die CPU als nächstes zugeteilt wird. Für gewöhnlich wird der Thread mit der höchsten Priorität ausgewählt. Warten dabei mehrere, gleichhoch-priore Threads auf die Zuteilung der CPU, wählt der Scheduler irgendeinen aus. Es gibt keine Garantie, dass der Thread gewählt wird, welcher am längsten wartet. Wie sich Prioritäten auf das Scheduling auswirken ist plattformabhängig, da die Spezifikation lediglich angibt, dass Threads Prioritäten haben müssen, aber vernachlässigt, was der Scheduler mit ihnen anfangen soll.

In java.lang.Thread sind drei Prioritäts-Konstanten definiert: MAX_PRIORITY, MIN_PRIORITY und NORM_PRIORITY. Mittels setPriority() lässt sich ein neuer Wert setzen, getPriority() liefert den aktuellen. Erzeugt ein Thread während seine Ausführung weitere Threads (Deamon Threads), erben diese seine Priorität.

Abb. 1: Mögliche Zustände eines Threads

Historisch bedingt gibt es zwei Ansätze für Scheduling-Strategien: Preemtive und Time Sliced (bzw. Round Robin). Beim preemtive Scheduling wird einem Thread die CPU entzogen sobald ein anderer mit höherer Priorität in den Zustand Ready übergeht. Beim time sliced Scheduling erhält jeder Thread ein gewisses Zeitquantum zur Ausführung. Nach dessen Ablauf kommt der nächste Thread an die Reihe, bis alle Threads einmal an der Reihe waren. Anschließend wird wieder beim Ersten begonnen. Time Slicing hat den Vorteil, dass nicht ein einziger hochpriorer Thread alle andern an deren Ausführung hindern kann, hat aber zugleich den Nachteil, dass so das System nicht deterministisch ist; ab einem gewissen Zeitpunkt ist nicht mehr vorhersagbar, welcher Thread gerade ausgeführt wird und wie lange dieser für seine Arbeit insgesamt benötigt.

Nun stellt sich berechtigterweise die Frage, welche Strategie die JVM verwendet. Auch hier ist die Antwort: das ist plattformabhängig. Soviel ist sicher: Für Solaris wird preemtive, für Macintosh time sliced und für Windows ebenfalls time sliced (ab JDK 1.0.2, vorher preemtive) Scheduling verwendet.

Yielding

Ein Thread kann das ihm zugeteilte Recht der Ausführung freiwillig abgeben, um anderen, auf die Ausführung wartenden Threads die Chance einzuräumen, aktiv zu werden. Dies wird mit dem Aufruf der Methode Thread.yield() erreicht. Befindet sich mindestens ein weiterer Thread im Zustand Ready, entzieht der Scheduler dem aufrufenden Thread die CPU, womit er sich ebenfalls im Zustand Ready befindet und entscheidet, welchem der anderen Threads diese zugeteilt wird. Es sei noch erwähnt, dass der Scheduler im Allgemeinen die Ausführung des aktuellen Threads nicht unterbricht, wenn sich lediglich Threads mit niedriger Priorität im Zustand Ready befinden. Dieses Verhalten ist aber plattformabhängig und damit nicht garantiert.

Yielding ist ein sehr nützliches Feature, welches bei arbeitsintensiven Threads zum Einsatz kommen sollte. Man stelle sich eine Anwendung mit GUI vor, mittels welcher unter anderem Daten aus mehreren Dateien importiert und in geeigneter Weise transformiert werden. Während des Imports und der Transformation sollen bestimmte Status-Informationen des Vorgangs in der GUI angezeigt werden (z.B. aktuell bearbeitete Datei, Fortschritt der Bearbeitung). Würde man die Ausführung nicht von Zeit zu Zeit unterbrechen, könnte der GUI-Thread weder die Status-Informationen auf dem Bildschirm anzeigen noch auf Eingaben des Benutzers reagieren, da der Import-Vorgang, sofern nicht durch I/O-Zugriffe blockiert, die CPU voll beansprucht.

Was ein Thread ausführt

Wenn ein Thread schläft, wartet er auf das Verstreichen einer definierten Zeit und geht anschließend in den Zustand Ready über. Da er nicht sofort wieder in den Zustand Running wechselt, sondern auf die Zuteilung der CPU wartet, kann lediglich garantiert werden, dass der Thread mindestens die definierte Zeit ruht bis er mit der Ausführung fortfährt. Mittels Thread.sleep() wird dieses Verhalten erreicht. Der aufrufende Thread legt sich dann für (mindestens) die angegeben Dauer schlafen. Je nachdem mit welcher Genauigkeit die Dauer angegeben werden soll, existieren zwei Methoden:

  • public static void sleep(long milliseconds)
  • throws InterruptedException
  • public static void sleep(long milliseconds, int nanoseconds) throws InterruptedException

Wie die Methodensignaturen zeigen, kann während des Schlafes eine InterruptedException ausgelöst werden, was wiederum das Erwachen des Threads zur Folge hat – er befindet sich dann im Zustand Ready. Diese wird natürlich nicht willkürlich geworfen, sondern dann, wenn ein anderer Thread dessen interrupt-Methode aufruft. Nach der Zuteilung der CPU wird der InterruptedException-Handler ausgeführt.

Abb. 2: Mögliche Zustände bei der Thread-Synchronisation

Java verwendet zur Synchronisation der Objekt-Zugriffe von Threads so genannte Monitore. Ein Monitor ist ein Objekt, welches Threads blockieren und wiederbeleben kann. Die Anwendung des Monitor-Konzeptes ist relativ einfach: Damit ein Thread seine Arbeit fortführen kann, ist unter Umständen ein bestimmter Zustand eines Objektes nötig. Befindet sich dieses Objekt nicht in diesem Zustand, wartet er solange bis dieses signalisiert, dass sich dessen Zustand geändert hat. Der Thread prüft nun, ob der gewünschte Zustand erreicht ist. Wenn ja, kann er das Objekt verwenden, wenn nicht, geht er wieder in den Wartezustand. Man denke an das oben genannte Socket-Beispiel: Es muss solange gewartet werden bis ein Byte empfangen wurde.
Java stellt dafür folgende Hilfsmittel zur Verfügung:

  • Genau einen Lock für jede Klasse
  • Genau einen Lock für jedes Objekt
  • Das Schlüsselwort synchronized
  • Die Methoden wait(), notify() und notifyAll(), der Klasse java.lang.Object

Jedes Objekt hat einen so genannten Lock. Dieses Schloss lässt sich nur unter Zuhilfenahme des Schlüssels synchronized verwenden. Es regelt gewissermaßen den Zugriff auf den Code eines Objektes, welcher mittels synchronized gesichert wurde. Dabei wird sichergestellt, dass zu einem Zeitpunkt jeweils nur ein Thread diesen ausführen darf, d.h. parallele Zugriffe werden synchronisiert, sodass diese sequentiell erfolgen. Dies ist nötig, um sicherzustellen, dass Objekte bzw. deren Daten immer konsistent bleiben – also z.B. nicht der Fall eintreten kann, dass ein Thread gerade die Daten eines Objektes ändert, währenddessen verdrängt wird und ein anderer diese teilweise veränderten und damit nicht korrekten Daten liest. Durch Synchronisation werden Zugriffe auf Objekte gewissermaßen unteilbar.

Möchte nun ein Thread den geschützten Code eines Objektes ausführen, muss er zunächst warten bis ihm der Lock für dieses Objekt zugeteilt wird – er befindet sich im Zustand Seeking-Lock. Nach dem Erhalt des Locks wechselt er in den Zustand Ready. Verlässt er den synchronisierten Code, wird der Lock wieder freigegeben. Ein Java-Programmierer bekommt davon selbstverständlich nichts mit. Das Einzige, um das dieser sich kümmern muss, ist anzugeben, welcher Bereich zu schützen ist. Dabei gibt es zwei Möglichkeiten:

public void aMethod(){
synchronized(this){
// ...
}
}

Weiterhin ist es möglich nur einen durch synchronized(Object o){} begrenzten Bereich vor parallelen Zugriffen zu schützen. Dabei wird das Objekt, dessen Lock zur Synchronisation herangezogen werden soll, als Parameter angegeben. Es ist zwar prinzipiell erlaubt, dennoch lässt sich kein vernünftiges Beispiel finden, bei welchem als Parameter ein anderes Objekt als this angegeben werden sollte.

Bislang wurden Locks für Klassen vernachlässigt. Diese werden zur Synchronisation von statischen Methoden benötigt. Dies ist, wenn überhaupt, ein mögliches Anwendungsgebiet zur Synchronisation mittels anderer Objekte als this, da es in einem statischen Kontext bekanntlich kein this gibt.

class StrangeSynchronization{
private final static Object mutex = new Object();
public static void aMethod(){
synchronized(mutex){
// ...
}
}
}

Das Beispiel zeigt, wie in statischem Kontext ein begrenzter Teil einer Methode synchronisiert werden kann. Das Synchronisations-Objekt wurde als final deklariert, damit zu jedem Zeitpunkt sichergestellt ist, dass es sich um ein und dasselbe Objekt handelt. Es bleibt allerdings abzuwägen, ob sich der Aufwand tatsächlich lohnt und nicht doch die herkömmliche Methode zur Synchronisation verwendet werden sollte – Synchronisation der gesamten Methode.

Das Begrenzen des zu synchronisierenden Codes ist immer dann ratsam, wenn eine Methode relativ viel leistet, aber eigentlich nur ein geringer Teil geschützt werden müsste. Die Einschränkung des zu synchronisierenden Codes kann so mitunter die gesamte Ausführungsgeschwindigkeit der Anwendung steigern, da parallel arbeitende Threads nun weniger lang warten müssen. Allerdings ist von Außen nun nicht mehr ersichtlich, dass in dieser Methode synchronisiert wird. Dies sollte in der Dokumentation (JavaDoc) zusätzlich angegeben werden.

Was ein Thread ausführt

Im Abschnitt Synchronizing wurde kurz erwähnt, was ein Monitor ist und wie dieser allgemein verwendet wird. In Java-Terminologie ist dagegen ein Monitor lediglich ein Objekt, welches synchronisierten Code besitzt.

Abpictureung 2 zeigt einen bislang noch nicht behandelten Zustand, welcher aber für das allgemeine Monitor-Konzept essentiell ist: Waiting. Ein Thread gelangt in den Warte-Zustand, wenn ihm das alleinige Zugriffsrecht für ein Objekt zugesprochen wurde, er sich also in synchronisiertem Code bewegt und er die Methode wait() dieses Objektes aufruft, um auf dessen Zustandsänderung zu warten. Dabei wird allerdings der Lock des Objektes wieder freigegeben. Sonst könnte kein anderer Thread mehr den geschützten Code betreten, um die erwartete Zustandsänderung herbeizuführen. Der Thread würde dann bis in alle Ewigkeit warten (Deadlock). Die Klasse java.lang.Object weist drei unterschiedliche Signaturen der wait-Methode auf. Sie bietet unter anderem die Möglichkeit, die Wartezeit zu begrenzen und auf Milli- bzw. Nanosekunden genau anzugeben. Im Normalfall wird jedoch die parameterlose Variante der Methode verwendet und zunächst für unbestimmte Zeit gewartet. Es führen genau drei Wege aus dem Warte-Zustand wieder heraus:

  • Ein anderer Thread signalisiert den Zustandwechsel mittels notify() bzw. notifyAll()
  • Die angegebene Zeit ist abgelaufen
  • Ein anderer Thread ruft die Methode interrupt() des wartenden Threads auf

Jedes Objekt verwaltet einen Pool von wartenden Threads, Wait-Set genannt. Ruft nun ein Thread notify() des Synchronisations-Objektes auf, wird ein Thread aus diesem Pool entfernt und in den Zustand Seeking Lock überführt. Er muss nun warten bis der Lock für dieses Objekt wieder freigegeben und ihm das Zugriffsrecht erteilt wird. Wie der Name vermuten lässt, wird bei notifyAll() nicht nur ein einziger, willkürlich ausgewählter Thread aus dem Pool entfernt, sondern alle. Diese sind nun in der Lage auf die Zustandsänderung des Objektes entsprechend zu reagieren.

Ähnlich wie beim Zustand Sleeping kann der Warte-Zustand von einem anderen Thread unterbrochen werden, in dem er die interrupt-Methode des wartenden Threads aufruft. Auch hier wird eine java.lang.InterruptedException geworfen. Der Thread befindet sich daraufhin im Zustand Seeking Lock. Nach Erhalt des Locks wird der entsprechende Exception-Handler ausgeführt.

public synchronized void aMethod(){
while( ... ){ // gewünschter Zustand ?
try{
wait();
}
catch(InterruptedException ie){ ... }
}

// ...
notifyAll();
}

Das Beispiel zeigt die konkrete Anwendung des Monitor-Konzeptes. Der Objekt-Zustand sollte immer in einer while-Schleife geprüft werden: Angenommen, ein Thread betritt die Methode aMethod(). Er bemerkt, dass der aktuelle Objekt-Zustand nicht der gewünschte ist und ruft daraufhin wait() auf. Nachdem der Zustandwechsel signalisiert wurde und er den Lock erhalten hat, prüft er abermals, ob es sich um den gewünschten Objekt-Zustand handelt. Hat das Objekt diesen eingenommen, kann er nun mit der Bearbeitung fortfahren, wenn nicht, geht er wieder in den Warte-Zustand. Würde in diesem Beispiel statt der while-Schleife lediglich eine if-Anweisung verwendet, könnte dies zu einem falschen Verhalten führen, nämlich dann, wenn sich das Objekt nicht in dem erwarteten Zustand befindet, z.B. wenn ein weiterer Thread das Zugriffsrecht vor ihm erhalten und das Objekt manipuliert hat.

Grundsätzlich kommt es auf die konkrete Anwendung an, ob notify() oder notifyAll() zur Signalisierung des Zustandswechsels verwendet wird. Sollen mehrere Threads auf einen Zustandswechsel reagieren, ist notifyAll() die richtige Wahl. Denn wie erwähnt erweckt notify()lediglich einen einzigen Thread, alle anderen bleiben im Warte-Zustand bis einer der oben genannten Fälle eintritt. Analog dazu sollte notify() immer dann angewandt werden, wenn lediglich ein nicht näher bestimmter Thread die Verarbeitung vornehmen soll.

Die Methoden wait(), notify() und notifyAll() sind nur innerhalb synchronisiertem Code erlaubt. Grundregel hier ist, dass ein Thread diese Methoden nur bei einem Objekt verwenden darf, dessen Lock er auch besitzt, d.h. die Synchronisation muss sich auf dieses Objekt beziehen. Missachtung dieser Vorschrift wird mit einer java.lang.IllegalMonitorStateException bestraft. Hierbei handelt es sich um eine RuntimeException, weshalb diese auch nicht abgefangen werden muss. Werden die Methoden wie in obigem Beispiel angewandt, wird dieser Fall auch nicht eintreten. Das folgende Beispiel zeigt einen Fall, der zu hundert Prozent mit einer Ausnahme geahndet wird:

class StrangeSynchronization_2{
private final Object mutex = new Object();

public void aMethod(){
synchronized(mutex){
mutex.notifyAll();
}
}
}

Das JDK stellt drei nützliche Utility-Klassen zur Verfügung, welche alle im Package java.lang zu finden sind und abschließend kurz vorgestellt werden sollen.

Die Klasse ThreadGroup ermöglicht das Gruppieren von Threads. ThreadGroups können sowohl Threads als auch wiederum ThreadGroups enthalten. Damit ist es auf einfache Weise möglich eine Hierarchie von Threads und Gruppen von Threads in Form eines Baumes zu implementieren. Zu den Key-Features gehört das Ermitteln aller aktiven Threads und Thread-Gruppen einer ThreadGroup sowie deren Eltern-Gruppe. Die Baumstruktur lässt sich denkbar einfach traversieren, um an einzelne Threads zu gelangen. Sie ist somit auch ein Hilfsmittel, um Referenzen von Threads zu verwalten und sinnvoll zu strukturieren.

Bislang gab es noch keine Möglichkeit Inhalte bzw. Werte von Variablen einem bestimmten Thread zuzuordnen, sodass zwar jeder Thread auf die gleiche Weise auf diese Variable zugreift, dennoch jeder seine eigene Kopie erhält und manipuliert. Man denke zum Beispiel an eine Anwendung, welche Transaktionen auszuführen hat und diese von Arbeits-Threads bearbeiten lässt. Ein Thread erhält nun den Auftrag eine Transaktion auszuführen. Mit einer Transaktion sind auch spezifische Informationen verknüpft, z.B. Transaktions-ID, und Ähnliches. Diese Informationen werden unter Umständen in verschiedenen Klassen (bzw. Methoden), welche in die Bearbeitung involviert sind, benötigt. Nun könnte man zum Beispiel diese Informationen als Objekt entlang der Methoden-Aufrufkette weiterreichen, um sie an entsprechender Stelle zu verwenden. Eleganter lässt sich das Problem mittels der Klasse ThreadLocal lösen, welche mit dem JDK 1.2 eingeführt wurde. Das folgende Beispiel soll die Verwendung von thread-lokalen Variablen verdeutlichen:

public class CounterThread extends Thread{
public void run(){
for(int i = 1; i 

Das Beispiel zeigt einen kurzen Abriss einer Klasse, welche Transaktions-Informationen verwaltet. Damit jede Methode, welche von einem Thread ausgeführt wird, Zugriff auf thread-lokale Variablen hat, sind diese normalerweise statisch. Die Klasse TransactionInfo soll die Transaktions-ID verwalten. Die Variable nextTransactionID enthält die nächste zu verwendende Transaktions-ID und ist, da es sich um eine gewöhnliche statische Variable handelt, für jeden Thread gleichermaßen sichtbar. Die ID einer bestimmten Transaktion wird als thread-lokale Variable gehalten. Variablen vom Typ ThreadLocal speichern lediglich die Referenz der eigentlichen Variablen. Im Beispiel wird eine statische Variable tid vom Typ ThreadLocal deklariert und initialisiert. Wobei die Initialisierungs-Methode initialValue() in Form einer anonymen Klasse überschrieben wird, sodass nun ein Integer-Objekt, welches den Wert von nextTransactionID erhält, gespeichert wird. Die ursprüngliche Methode setzt die Referenz lediglich auf null, d.h. die Initialisierung müsste an anderer Stelle manuell durch ThreadLocal.set() erfolgen. Nun kann mittels TransactionInfo.getTransactionID() die Transaktions-ID in Erfahrung gebracht werden. Normalerweise würde man annehmen, dass für jeden Thread die gleiche Transaktions-ID zurückgeliefert wird. Dem ist allerdings nicht so, da wie gesagt die Transaktions-ID für jeden Thread separat verwaltet wird. Sie wird mit dem ersten Aufruf von getTransactionID() initialisiert und bleibt in diesem Beispiel auch für weitere Aufrufe unverändert.

Für den Fall, dass ein Thread weitere Threads erzeugt und diese die Werte von dessen thread-lokalen Variablen übertragen bekommen sollen, wurde die Klasse InheritableThreadLocal geschaffen, d.h. bei der Erzeugung bekommen alle Kinder-Threads die Werte der vererbbaren thread-lokalen Variablen mitgeliefert. Um eine andere Initialisierung als mit den Werten des Vater-Threads zu erzwingen, ist die Methode childValue() der Klasse InheritableThreadLocal entsprechend zu überschreiben. Beispielsweise könnte man obige Anwendung so erweitern, dass eine Transaktion in mehrere Sub-Transaktionen gegliedert werden kann. Um jede Sub-Transaktion kümmert sich dann ebenfalls ein separater Thread. Um die Transaktions-ID vererbbar zu machen, genügt es den Typ von tid durch InheritableThreadLocal zu ersetzen. Damit gehört jede Sub-Transaktion zu einer bestimmten (Haupt-) Transaktion, deren ID durch getTransactionID() ermittelt werden kann.

Was ein Thread ausführt

Im Alltag eines Java-Programmierers bleibt es nicht aus, dass man sich über kurz oder lang mit Threads auseinandersetzen muss. Seien es Anwendungen mit anspruchsvollen GUIs, Web-Applikationen, Client-/Server-Architekturen allgemein oder Ähnliches, überall trifft man auf Multi-Threaded-Umgebungen. Um derartige Anwendungen erfolgreich pflegen, weiterentwickeln oder gar neue Systeme ersinnen zu können, ist es unerlässlich nicht nur das Konzept von Threads verstanden zu haben, sondern auch dessen konkrete Anwendung zumindest einmal auszuprobieren.

Geschrieben von
Mike Werner
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.