Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Sprachbeschreibung
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Mathematisches
6 Eigene Klassen schreiben
7 Angewandte Objektorientierung
8 Exceptions
9 Generics, innere Klassen
10 Die Klassenbibliothek
11 Threads und nebenläufige Programmierung
12 Datenstrukturen und Algorithmen
13 Raum und Zeit
14 Dateien und Datenströme
15 Die eXtensible Markup Language (XML)
16 Grafische Oberflächen mit Swing
17 Grafikprogrammierung
18 Netzwerkprogrammierung
19 Verteilte Programmierung mit RMI und Web–Services
20 JavaServer Pages und Servlets
21 Applets
22 Midlets und die Java ME
23 Datenbankmanagement mit JDBC
24 Reflection und Annotationen
25 Logging und Monitoring
26 Sicherheitskonzepte
27 Java Native Interface (JNI)
28 Dienstprogramme für die Java-Umgebung
Stichwort

Download:
- ZIP, ca. 14,1 MB
Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Java ist auch eine Insel (8. Auflage) von Christian Ullenboom
Programmieren mit der Java Standard Edition Version 6
Buch: Java ist auch eine Insel (8. Auflage)

Java ist auch eine Insel (8. Aufl.)
8., aktual. Auflage, geb., mit DVD
1.475 S., 49,90 Euro
Galileo Computing
ISBN 978-3-8362-1371-4
Pfeil 11 Threads und nebenläufige Programmierung
Pfeil 11.1 Nebenläufigkeit
Pfeil 11.1.1 Threads und Prozesse
Pfeil 11.1.2 Wie parallele Programme die Geschwindigkeit steigern können
Pfeil 11.1.3 Was Java für Nebenläufigkeit alles bietet
Pfeil 11.2 Threads erzeugen
Pfeil 11.2.1 Threads über die Schnittstelle Runnable implementieren
Pfeil 11.2.2 Thread mit Runnable starten
Pfeil 11.2.3 Der Name eines Threads
Pfeil 11.2.4 Die Klasse Thread erweitern
Pfeil 11.2.5 Wer bin ich?
Pfeil 11.3 Die Zustände eines Threads
Pfeil 11.3.1 Threads schlafen
Pfeil 11.3.2 Mit yield() auf Rechenzeit verzichten
Pfeil 11.3.3 Das Ende eines Threads
Pfeil 11.3.4 UncaughtExceptionHandler für unbehandelte Ausnahmen
Pfeil 11.3.5 Einen Thread höflich mit Interrupt beenden
Pfeil 11.3.6 Der stop() von außen und die Rettung mit ThreadDeath
Pfeil 11.3.7 Ein Rendezvous mit join()
Pfeil 11.3.8 Barrier und Austausch mit Exchanger
Pfeil 11.3.9 Arbeit niederlegen und wieder aufnehmen
Pfeil 11.3.10 Priorität
Pfeil 11.3.11 Der Thread als Dämon
Pfeil 11.4 Der Ausführer (Executor) kommt
Pfeil 11.4.1 Die Schnittstelle Executor
Pfeil 11.4.2 Die Thread-Pools
Pfeil 11.4.3 Threads mit Rückgabe über Callable
Pfeil 11.4.4 Mehrere Callable abarbeiten
Pfeil 11.4.5 Mit ScheduledExecutorService wiederholende Ausgaben und Zeitsteuerungen
Pfeil 11.5 Synchronisation über kritische Abschnitte
Pfeil 11.5.1 Gemeinsam genutzte Daten
Pfeil 11.5.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
Pfeil 11.5.3 Punkte parallel initialisieren
Pfeil 11.5.4 i++ sieht atomar aus, ist es aber nicht
Pfeil 11.5.5 Kritische Abschnitte schützen
Pfeil 11.5.6 Schützen mit ReentrantLock
Pfeil 11.5.7 Synchronisieren mit synchronized
Pfeil 11.5.8 Synchronized-Methoden der Klasse StringBuffer
Pfeil 11.5.9 Mit synchronized synchronisierte Blöcke
Pfeil 11.5.10 Dann machen wir doch gleich alles synchronisiert!
Pfeil 11.5.11 Lock-Freigabe im Fall von Exceptions
Pfeil 11.5.12 Mit synchronized nachträglich synchronisieren
Pfeil 11.5.13 Monitore sind reentrant – gut für die Geschwindigkeit
Pfeil 11.5.14 Synchronisierte Methodenaufrufe zusammenfassen
Pfeil 11.5.15 Deadlocks
Pfeil 11.6 Synchronisation über Warten und Benachrichtigen
Pfeil 11.6.1 Die Schnittstelle Condition
Pfeil 11.6.2 It’s Disco-Time
Pfeil 11.6.3 Warten mit wait() und Aufwecken mit notify()
Pfeil 11.6.4 Falls der Lock fehlt: IllegalMonitorStateException
Pfeil 11.6.5 Semaphor
Pfeil 11.7 Atomare Operationen und frische Werte mit volatile
Pfeil 11.7.1 Der Modifizierer volatile bei Objekt-/Klassenvariablen
Pfeil 11.7.2 Das Paket java.util.concurrent.atomic
Pfeil 11.8 Mit dem Thread verbundene Variablen
Pfeil 11.8.1 ThreadLocal
Pfeil 11.8.2 InheritableThreadLocal
Pfeil 11.9 Gruppen von Threads in einer Thread-Gruppe
Pfeil 11.9.1 Aktive Threads in der Umgebung
Pfeil 11.9.2 Etwas über die aktuelle Thread-Gruppe herausfinden
Pfeil 11.9.3 Threads in einer Thread-Gruppe anlegen
Pfeil 11.9.4 Methoden von Thread und ThreadGroup im Vergleich
Pfeil 11.10 Zeitgesteuerte Abläufe
Pfeil 11.10.1 Die Klassen Timer und TimerTask
Pfeil 11.10.2 Job-Scheduler Quartz
Pfeil 11.11 Einen Abbruch der virtuellen Maschine erkennen
Pfeil 11.12 Zum Weiterlesen


Galileo Computing - Zum Seitenanfang

11.5 Synchronisation über kritische Abschnitte Zur nächsten ÜberschriftZur vorigen Überschrift

Wenn Threads in Java ein eigenständiges Leben führen, ist dieser Lebensstil nicht immer unproblematisch für andere Threads, insbesondere beim Zugriff auf gemeinsam genutzte Ressourcen. In den folgenden Abschnitten erfahren wir mehr über gemeinsam genutzte Daten und Schutzmaßnahmen beim konkurrierenden Zugriff durch mehrere Threads.


Galileo Computing - Zum Seitenanfang

11.5.1 Gemeinsam genutzte Daten Zur nächsten ÜberschriftZur vorigen Überschrift

Ein Thread besitzt zum einen seine eigenen Variablen, etwa die Objektvariablen, kann aber auch statische Variablen nutzen, wie das folgende Beispiel zeigt:

class T extends Thread 
{ 
  static int result; 
 
  public void run() { ... } 
}

In diesem Fall können verschiedene Exemplare der Klasse T, die jeweils einen Thread bilden, Daten austauschen, indem sie die Informationen in result ablegen oder daraus entnehmen. Threads können aber auch an einer zentralen Stelle eine Datenstruktur erfragen und dort Informationen entnehmen oder Zugriff auf gemeinsame Objekte über eine Referenz bekommen. Es gibt also viele Möglichkeiten, wie Threads – und damit potenziell parallel ablaufende Aktivitäten – Daten austauschen können.


Galileo Computing - Zum Seitenanfang

11.5.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte Zur nächsten ÜberschriftZur vorigen Überschrift

Da Threads ihre eigenen Daten verwalten – sie haben alle eigene lokale Variablen und einen Stack –, kommen sie sich gegenseitig nicht in die Quere. Auch wenn mehrere Threads gemeinsame Daten nur lesen, ist das unbedenklich; Schreiboperationen sind jedoch kritisch. Wenn sich zehn Nutzer einen Drucker teilen, der die Ausdrucke nicht als unteilbare Einheit bündelt, lässt sich leicht ausmalen, wie das Ergebnis aussieht. Seiten, Zeilen oder gar einzelne Zeichen aus verschiedenen Druckaufträgen werden bunt gemischt ausgedruckt.

Die Probleme haben ihren Ursprung in der Art und Weise, wie die Threads umgeschaltet werden. Der Scheduler unterbricht zu einem uns unbekannten Zeitpunkt die Abarbeitung eines Threads und lässt den nächsten arbeiten. Wenn nun der erste Thread gerade Programmzeilen abarbeitet, die zusammengehören, und der zweite Thread beginnt, parallel auf diesen Daten zu arbeiten, ist der Ärger vorprogrammiert. Wir müssen also Folgendes ausdrücken können: Wenn ich den Job mache, dann möchte ich der Einzige sein, der die Ressource – etwa einen Drucker – nutzt. Erst nachdem der Drucker den Auftrag eines Benutzers fertiggestellt hat, darf er den nächsten in Angriff nehmen.

Kritische Abschnitte

Zusammenhängende Programmblöcke, die nicht unterbrochen werden dürfen und besonders geschützt werden müssen, nennen sich kritische Abschnitte. Wenn lediglich ein Thread den Programmteil abarbeitet, dann nennen wir dies gegenseitigen Ausschluss oder atomar. Wir könnten das etwas lockerer sehen, wenn wir wissen, dass innerhalb der Programmblöcke nur von den Daten gelesen wird. Sobald aber nur ein Thread Änderungen vornehmen möchte, ist ein Schutz nötig. Wir werden uns nun Beispiele für kritische Abschnitte anschauen und dann sehen, wie wir diese in Java realisieren können.

Nicht kritische Abschnitte

Wenn mehrere Threads auf das gleiche Programmstück zugreifen, muss das nicht zwangsläufig zu einem Problem führen. Immutable Objekte – nehmen wir an, ein Konstruktor belegt einmalig die Zustände – müssen nicht Thread-sicher sein, da es keine Schreibzugriffe gibt und bei Lesezugriffen nichts schiefgehen kann. Immutable-Klassen wie String oder Wrapper-Klassen kommen daher ohne Synchronisierung aus.

Das Gleiche gilt für Methoden, die keine Objekteigenschaften verändern. Da jeder Thread seine Thread-eigenen Variablen besitzt – jeder Thread hat einen eigenen Stack –, können lokale Variablen, auch Parametervariablen, beliebig gelesen und geschrieben werden. Wenn zum Beispiel zwei Threads die folgende Utility-Funktion aufrufen, ist das kein Problem:

public static String reverse( String s ) 
{ 
  return new StringBuilder( s ).reverse().toString(); 
}

Jeder Thread wird eine eigene Variablenbelegung für s haben und ein temporäres Objekt vom Typ StringBuilder referenzieren.


Hinweis Die Stack-Größe eines Threads bestimmt bei der Sun JVM der Schalter xss oder xX:ThreadStackSize. Ein Beispiel: Setze die maximale Größe des Stacks auf 2 MB: -Xss2048k.



Galileo Computing - Zum Seitenanfang

11.5.3 Punkte parallel initialisieren Zur nächsten ÜberschriftZur vorigen Überschrift

Nehmen wir an, ein Thread T1 belegt das vorher mit (0,0) belegte Point-Objekt p mit den Werten (1,2) und ein Thread T2 gleichzeitig mit den Werten (2,1). Das bedeutet: T1 führt die Anweisungen

p.x = 1; p.y = 2;

durch und T2 die Anweisungen:

p.x = 2; p.y = 1;

Jetzt ist es möglich, dass T1 mit der Arbeit beginnt und x = 1 setzt. T1 wird nun unterbrochen, und T2 kommt an die Reihe. Dieser überschreibt x = 2 und setzt auch y = 1. Jetzt darf T1 weitermachen und vervollständigt y = 2. Wir erkennen das nicht beabsichtigte Ergebnis (2,2), es könnte aber auch (1,1) sein, wenn wir das gleiche Szenario beginnend mit T2 durchführen. Je nach zuerst abgearbeitetem Thread wäre jedoch entweder (1,2) oder (2,1) wünschenswert. Die Threads sollen ihre Arbeit also atomar erledigen, und die Zuweisung bildet einen kritischen Abschnitt, der geschützt werden muss. Standardmäßig sind die zwei Zuweisungen nicht-atomare Operationen.

Um dies an einem Beispiel zu zeigen, sollen zwei Threads ein Point-Objekt verändern. Die Threads belegen x und y immer gleich, und immer dann, wenn sich die Koordinaten unterscheiden, soll es eine Meldung geben:

Listing 11.14 com/tutego/insel/thread/concurrent/ParallelPointInit.java, main()

final Point p = new Point(); 
 
Runnable r = new Runnable() 
{ 
  @Override public void run() 
  { 
    int x = (int)(Math.random() * 1000), y = x; 
 
    while ( true ) 
    { 
      p.x = x; p.y = y;            // * 
      int xc = p.x, yc = p.y;      // * 
 
      if ( xc != yc ) 
        System.out.println( "Aha: x=" + xc + ", y=" + yc ); 
    } 
  } 
}; 
 
new Thread( r ).start(); 
new Thread( r ).start();

Die interessanten Zeilen sind mit * markiert. p.x = x; p.y = y; belegt die Koordinaten neu, und int xc = p.x, yc = p.y; liest die Koordinaten erneut aus. Würden Belegung und Auslesen in einem Rutsch passieren, dürfte überhaupt keine unterschiedliche Belegung von x und y zu finden sein. Doch das Beispiel zeigt es anders!

Aha: x=58, y=116 
Aha: x=116, y=58 
Aha: x=58, y=116 
Aha: x=58, y=116 
...

Was wir mit den parallelen Punkten vor uns haben, sind Effekte, die von den Ausführungszeiten der einzelnen Operationen abhängen. In Abhängigkeit von dem Ort der Unterbrechung wird ein fehlerhaftes Verhalten produziert. Dieses Szenario nennt sich im Englischen race condition beziehungsweise race hazard (zu Deutsch auch Wettlaufsituation).


Galileo Computing - Zum Seitenanfang

11.5.4 i++ sieht atomar aus, ist es aber nicht Zur nächsten ÜberschriftZur vorigen Überschrift

Das Beispiel vorhin ist plastisch und einleuchtend, weil zwischen Anweisungen unterbrochen werden kann. Das Problem liegt aber noch tiefer. Schon einfache Anweisungen wie i++ müssen geschützt werden. Um dies zu verstehen, wollen wir einen Blick auf folgende Zeilen werfen:

Listing 11.15 com/tutego/insel/thread/IPlusPlus.java, IPlusPlus

public class IPlusPlus 
{ 
  static int i; 
  static void foo() 
  { 
   i++; 
  } 
}

Die Objektmethode foo() erhöht die statische Variable i. Um zu erkennen, dass i++ ein kritischer Abschnitt ist, sehen wir uns den dazu generierten Bytecode [Machbar zum Beispiel mit dem jeder Java-Distribution beiliegenden Dienstprogramm javap und der Option -c.] für die Methode foo() an:

0 getstatic #19 <Field int i> 
3 iconst_1 
4 iadd 
5 putstatic #19 <Field int i> 
8 return

Die einfach aussehende Operation i++ ist also etwas komplizierter. Zuerst wird i gelesen und auf dem Stack abgelegt. Danach wird die Konstante 1 auf den Stack gelegt, und anschließend addiert iadd beide Werte. Das Ergebnis steht wiederum auf dem Stack und wird von putstatic zurück in i geschrieben.

Wenn jetzt auf die Variable i von zwei Threads A und B gleichzeitig zugegriffen wird, kann folgende Situation eintreten:

  • Thread A holt sich den Wert von i in den internen Speicher, wird dann aber unterbrochen. Er kann das um 1 erhöhte Resultat nicht wieder i zuweisen.
  • Nach der Unterbrechung von A kommt Thread B an die Reihe. Auch er besorgt sich i, kann aber i + 1 berechnen und das Ergebnis in i ablegen. Dann ist B beendet, und der Scheduler beachtet Thread A.
  • Jetzt steht in i das von Thread B um 1 erhöhte i. Thread A addiert nun 1 zu dem gespeicherten alten Wert von i und schreibt dann nochmals denselben Wert wie Thread B zuvor. Insgesamt wurde die Variable i nur um 1 erhöht, obwohl zweimal inkrementiert werden sollte. Jeder Thread hat für sich gesehen das korrekte Ergebnis berechnet.

Wenn wir die Funktion foo() atomar ausführen, haben wir das Problem nicht mehr, weil das Lesen aus i und das Schreiben zusammen einen unteilbaren, kritischen Abschnitt bilden.


Galileo Computing - Zum Seitenanfang

11.5.5 Kritische Abschnitte schützen Zur nächsten ÜberschriftZur vorigen Überschrift

Soll die Laufzeitumgebung nur einen Thread in einen Block lassen, nutzt Java einen Monitor [Der Begriff geht auf C. A. R. Hoare zurück, der im Aufsatz »Communicating Sequential Processes« von 1978 erstmals dieses Konzept veröffentlichte.] . Ein Monitor wird mit Hilfe eines Lock (zu Deutsch »Schloss«) realisiert, welches ein Thread öffnen oder schließen kann. Jedes Objekt in Java ist implizit mit einem Monitor verbunden. Tritt ein Thread in den kritischen Abschnitt ein, können wir uns vorstellen, dass die virtuelle Maschine den Programmcode wie eine Tür abschließt (engl. lock). Erst wenn der Abschnitt durchlaufen wurde, schließt die JVM die Tür wieder auf (engl. unlock), und ein anderer Thread kann den Abschnitt betreten. Das Ein- und Austreten wird von der Java-Maschine übernommen, und wir müssen das nicht kontrollieren. Kommt ein zweiter Thread zu dem Abschnitt, aber ein anderer Thread hat das Objekt schon verriegelt, muss er so lange warten, bis die Markierung gelöscht ist. Erst dann wird er hineingelassen. So ist die Abarbeitung über mehrere Threads einfach synchronisiert, und das Konzept eines Monitors gewährleistet seriellen Zugriff auf kritische Ressourcen.

Ein anschauliches Alltagsbeispiel ist unschwer zu finden, wenn wir schon einmal bei dem Vergleich mit der Tür sind. Gehen wir aufs Klo, um unseren Job zu machen, schließen wir die Tür hinter uns. Möchte jemand anders auf die Toilette, muss er warten. Es kann sich vor dem Klosett dann eine Schlange bilden. Genauso wartet ein Thread wegen eines belegten Objekts auf seinen Eintritt in den synchronisierten Block wie ein Wartender vor dem Klo, auch wenn der auf der Toilette Sitzende nach einer langen Nacht einnickt. Mit dem Abschließen und Aufschließen werden wir uns noch intensiver in den folgenden Abschnitten beschäftigen.


Hinweis Ein anderes Wort für Lock ist Mutex (engl. mutual exclusion, also »gegenseitiger Ausschluss«). Der Begriff »Monitor« wird oft gleichgesetzt mit Lock (Mutex), doch kann ein Monitor mit Warten/Benachrichtigen mehr als ein klassischer Lock. In der Definition der Sprache Java (JLS Kapitel 17) tauchen die Begriffe Mutex und Lock allerdings nicht auf; die Autoren sprechen nur von den Monitor-Aktionen lock und unlock. Die Java Virtual Machine definiert dafür die Opcodes monitorenter und monitorexit.


Java-Konstrukte zum Schutz der kritischen Abschnitte

Soll ein Programmabschnitt oder eine Objekt- oder Klassenmethode atomar ablaufen, gibt es dafür in Java zwei Möglichkeiten:

  • ein Sprachkonstrukt mit dem Schlüsselwort synchronized
  • Klassen aus dem in Java 5 eingeführten Paket java.util.concurrent.lock, die explizites Locking ermöglichen

Wenn wir auf unser Punkte-Problem zurückkommen, so stellen wir fest, dass zwei Zeilen auf eine Variable zugreifen:

p.x = x; p.y = y; 
int xc = p.x, yc = p.y;

Das Problem ist lösbar, wenn der Zugriff auf den Punkt nur über jeweils einen Thread erfolgt. Wenn also einer der Threads mit p.x = x beginnt, muss er so lange den exklusiven Zugriff bekommen, bis er mit yc = p.y endet.


Galileo Computing - Zum Seitenanfang

11.5.6 Schützen mit ReentrantLock Zur nächsten ÜberschriftZur vorigen Überschrift

Seit Java 5 gibt es die Schnittstelle Lock, mit dem sich ein kritischer Block markieren lässt. Ein Abschnitt beginnt mit lock() und endet mit unlock().

Listing 11.16 com/tutego/insel/thread/concurrent/ParallelPointInitSync.java, main()

final Lock lock = new ReentrantLock(); 
final Point p = new Point(); 
 
Runnable r = new Runnable() 
{ 
  @Override public void run() 
  { 
    int x = (int)(Math.random() * 1000), y = x; 
 
    while ( true ) 
    { 
      lock.lock(); 
 
      p.x = x; p.y = y;            // * 
      int xc = p.x, yc = p.y;      // * 
 
      lock.unlock(); 
 
      if ( xc != yc ) 
        System.out.println( "Aha: x=" + xc + ", y=" + yc ); 
    } 
  } 
}; 
 
new Thread( r ).start(); 
new Thread( r ).start();

Mit dieser Implementierung wird keine Ausgabe auf dem Bildschirm folgen.

Die Schnittstelle java.util.concurrent.locks.Lock

Lock ist eine Schnittstelle, von der ReentrantLock die wichtigste Implementierung ist. Mit ihr lässt sich der Block betreten und verlassen.


interface java.util.concurrent.locks.Lock


  • void lock()
    Wartet so lange, bis der kritische Abschnitt betreten werden kann, und markiert ihn dann als betreten.
  • boolean tryLock()
    Wenn der kritische Abschnitt sofort betreten werden kann, ist die Funktionalität wie bei lock() und die Rückgabe ist true. Ist der Lock gesetzt, so wartet die Methode nicht wie lock(), sondern kehrt mit einem false zurück.
  • boolean tryLock( long time, TimeUnit unit ) throws InterruptedException
    Versucht in der angegebenen Zeitspanne den Lock zu bekommen. Das Warten kann mit interrupt() auf dem Thread unterbrochen werden, was tryLock() mit einer Exception beendet.
  • void unlock()
    Verlässt den kritischen Block.
  • void lockInterruptibly() throws InterruptedException
    Wartet wie lock(), um den kritischen Abschnitt betreten zu dürfen, kann aber mit einem interrupt() von außen abgebrochen werden. (Der lock()-Methode ist ein Interrupt egal.) Implementierende Klassen müssen diese Vorgabe nicht zwingend umsetzen, sondern können die Methode auch mit einem einfachen lock() realisieren. ReentrantLock implementiert lockInterruptibly() erwartungsgemäß.

Beispiel Wenn wir sofort in den kritischen Abschnitt gehen können, tun wir das; sonst etwas anderes:

Lock lock = ...; 
if ( lock.tryLock() ) 
{ 
  try { 
    ... 
  } 
  finally { lock.unlock(); } 
} 
else 
  ...

Die Implementierung ReentrantLock kann noch ein bisschen mehr als lock() und unlock():


class java.util.concurrent.locks.ReentrantLock implements Lock, Serializable


  • ReentrantLock()
    Erzeugt ein neues Lock-Objekt, das nicht dem am längsten Wartenden den ersten Zugriff gibt.
  • ReentrantLock( boolean fair )
    Erzeugt ein neues Lock-Objekt mit fairem Zugriff, gibt also dem am längsten Wartenden den ersten Zugriff.
  • boolean isLocked()
    Anfrage, ob der Lock gerade genutzt wird und im Moment kein Betreten möglich ist.
  • final int getQueueLength()
    Ermittelt, wie viele auf das Betreten des Blocks warten.
  • int getHoldCount()
    Gibt die Anzahl der erfolgreichen lock()-Aufrufe ohne passendes unlock() zurück. Sollte nach Beenden des Vorgangs 0 sein.

Beispiel Das Warten auf den Lock kann unterbrochen werden:

Lock l = new ReentrantLock(); 
try { 
  l.lockInterruptibly(); 
  try { 
   ... 
  } 
  finally { l.unlock(); } 
} 
catch ( InterruptedException e ) { ... }

Wenn wir den Lock nicht bekommen haben, dürfen wir ihn auch nicht freigeben!


ReentrantReadWriteLock

Unsere Klasse ReentrantLock blockt bei jedem lock() und lässt keinen Interessenten in den kritischen Abschnitt. Viele Szenarien sind jedoch nicht so streng, und so kommt es zu Situationen, in denen lesender Zugriff durchaus von mehreren Parteien möglich ist, schreibender Zugriff aber blockiert wird.

Für diese Lock-Situation gibt es die Schnittstelle ReadWriteLock, die nicht von Lock abgeleitet ist, sondern mit readLock() und writeLock() die Lock-Objekte liefert. Die bisher einzige Implementierung der Schnittstelle ist java.util.concurrent.locks.ReentrantReadWriteLock. Ein Programmausschnitt könnte so aussehen:

ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 
try { 
  lock.readLock().lock(); 
  ... 
} finally { 
  lock.readLock().unlock(); 
}

Nebenläufig auf einen Assoziativspeicher zugreifen

Ein Assoziativspeicher ist eine Datenstruktur, die ein Objekt mit einem beliebigen anderen verbinden kann. HashMap ist eine Java-Klasse, die über put() eine Assoziation erreicht und über get() erfragt. Wir wollen nun über ReentrantReadWriteLock die Leseoperationen pa-rallel ermöglichen, aber Schreiboperationen atomar ausführen.

Listing 11.17 com/tutego/insel/thread/concurrent/ParallelMap.java

package com.tutego.insel.thread.concurrent; 
 
import java.util.*; 
import java.util.concurrent.locks.*; 
 
public class ParallelMap<K,V> 
{ 
  private final HashMap<K, V>  map = new HashMap<K,V>(); 
  private final ReadWriteLock lock = new ReentrantReadWriteLock(); 
  private final Lock      readLock = lock.readLock(), writeLock = lock.writeLock(); 
 
  public V get( K key ) 
  { 
    readLock.lock(); 
    try { 
      return map.get( key ); 
    } 
    finally { 
      readLock.unlock(); 
    } 
  } 
 
  public V put( K key, V value ) 
  { 
    writeLock.lock(); 
    try { 
      return map.put( key, value ); 
    } 
    finally { 
      writeLock.unlock(); 
    } 
  } 
 
  public void clear() 
  { 
    writeLock.lock(); 
    try { 
      map.clear(); 
    } 
    finally { 
      writeLock.unlock(); 
    } 
  } 
}

Galileo Computing - Zum Seitenanfang

11.5.7 Synchronisieren mit synchronized Zur nächsten ÜberschriftZur vorigen Überschrift

Schon seit Java 1.0 können kritische Abschnitte mit synchronized geschützt werden. Im einfachsten Fall markiert der Modifizierer synchronized die gesamte Methode. Ein betretender Thread setzt bei Objektfunktionen den Monitor des this-Objekts und bei statischen Methoden den Lock des dazugehörigen Class-Objekts.

Betritt ein Thread A eine synchronisierte Methode eines Objekts O und versucht anschließend Thread B eine synchronisierte Methode des gleichen Objekts O aufzurufen, muss der nachfolgende Thread B so lange warten, bis A wieder aus dem synchronisierten Teil austritt. Das geschieht, wenn der erste Thread A die Methode verlässt, denn mit dem Verlassen einer Methode – oder auch einer Ausnahme – gibt die JVM automatisch den Lock frei. Die Dauer eines Locks hängt folglich mit der Dauer des Methodenaufrufs zusammen, was zur Konsequenz hat, dass längere kritische Abschnitte die Parallelität einschränken und zu längeren Wartezeiten führen. Eine Endlosschleife in der synchronisierten Methode gäbe den Lock niemals frei.

Das aus IPlusPlus.java bekannte Problem mit dem i++ lässt sich mit synchronized einfach lösen:

synchronized void foo() { i++; }

Bei einem Konflikt (mehrere Threads rufen foo() auf) verhindert synchronized, dass sich mehr als ein Thread gleichzeitig im kritischen Abschnitt, dem Rumpf der Funktion foo(), befinden kann. Dies bezieht sich nur auf mehrere Aufrufe von foo() für dasselbe Objekt. Zwei verschiedene Threads können durchaus parallel die Funktion foo() für unterschiedliche Objekte ausführen.

Neben diesem speziellen Problem für atomares Verändern von Variablen lassen sich auch Klassen aus dem Paket java.util.concurrent.atomic verwenden – sie werden im Abschnitt 11.7.2 vorgestellt.

Eclipse-Icon Bei einem orthografisch anspruchsvollen Wort wie synchronized ist es praktisch, dass Eclipse auch Schlüsselwörter vervollständigt. Hier reicht ein Tippen von sync und Strg + Leertaste für einen Dialog.

Hat der aktuelle Thread den Lock?

Die statische Funktion Thread.holdsLock() zeigt an, ob der aktuelle Thread den Lock hält.

Listing 11.18 com/tutego/insel/thread/HoldsLockDemo.java, main()

final Object obj = new Object(); 
System.out.println( Thread.holdsLock(obj) );     // false 
synchronized ( obj ) 
{ 
  System.out.println( Thread.holdsLock(obj) );   // true 
}

Und Thread.holdsLock(this) wird etwa in einer Objektmethode feststellen können, ob der Lock durch eine synchronisierte Methode oder einen synchronized(this)-Block gelockt ist.


Galileo Computing - Zum Seitenanfang

11.5.8 Synchronized-Methoden der Klasse StringBuffer Zur nächsten ÜberschriftZur vorigen Überschrift

Wir wollen uns anhand einiger Beispiele noch ansehen, an welchen Objekten der Monitor beziehungsweise Lock gespeichert wird. Zunächst betrachten wir die Methode charAt() der Klasse StringBuffer und versuchen zu verstehen, warum die Methode synchronized ist.

public synchronized char charAt( int index ) 
{ 
  if ( (index < 0) || (index >= count) ) 
    throw new StringIndexOutOfBoundsException( index ); 
 
  return value[index]; 
}

Neben charAt() sind noch eine ganze Reihe anderer Methoden synchronisiert, etwa getChars(), setCharAt() und append(). Bei einer synchronized-Methode wird also der Lock bei einem konkreten StringBuffer-Objekt gespeichert. Wäre die Methode charAt() nicht atomar, dann könnte es passieren, dass durch Multithreading zwei Threads das gleiche StringBuffer-Objekt bearbeiten. Probleme können sich zum Beispiel dann ergeben, wenn ein Thread gerade den String verkleinert und gleichzeitig charAt() aufgerufen wird. Auch wenn zuerst charAt() einen gültigen Index feststellt, dann aber der StringBuffer verkleinert wird, gibt es ein Problem. Dann wäre nämlich der Index ungültig und value[index] fehlerhaft. Da aber charAt() synchronisiert ist, kann kein anderer Thread dasselbe StringBuffer-Objekt über synchronisierte Methoden modifizieren.


Beispiel Das StringBuffer-Objekt sb1 wird von zwei Threads T1 und T2 bearbeitet, indem synchronisierte Methoden genutzt werden. Bearbeitet Thread T1 den StringBuffer sb1 mit einer synchronisierten Methode, dann kann T2 erst dann eine synchronisierte Methode für sb1 aufrufen, wenn T1 die Methode abgearbeitet hat.

Denn T1 setzt bei sb1 die Sperre, die T2 warten lässt. Gleichzeitig kann aber T2 synchronisierte Methoden für ein anderes StringBuffer-Objekt sb2 aufrufen, da sb2 einen eigenen Monitor besitzt. Das macht noch einmal deutlich, dass die Locks zu einem Objekt gehören und nicht zur synchronisierten Methode.



Galileo Computing - Zum Seitenanfang

11.5.9 Mit synchronized synchronisierte Blöcke Zur nächsten ÜberschriftZur vorigen Überschrift

Wenn wir mit Lock-Objekten arbeiten, können wir den Block so fein wählen, wie es erforderlich ist. Mit synchronized haben wir bisher nur eine gesamte Methode sperren können, was in manchen Fällen etwas viel ist. Dann kann eine allgemeinere Variante in Java eingesetzt werden, die nur einen Block synchronisiert. Dazu schreiben wir in Java Folgendes:

synchronized ( objektMitDemMonitor ) 
{ 
  ... 
}

Der Block wird in die geforderten geschweiften Klammern gesetzt, und hinter dem Schlüsselwort in Klammern muss ein Objekt stehen, das den zu verwendenden Monitor besitzt. Die Konsequenz ist die Möglichkeit, über einen beliebigen Monitor zu synchronisieren und nicht unbedingt über den Monitor des Objekts, für das die synchronisierte Methode aufgerufen wurde, wie es bei synchronisierten Objektmethoden üblich ist.


Hinweis Eine synchronisierte Objektmethode ist nichts anderes als eine Variante von:

synchronized( this ) 
{ 
  // Code der Methode. 
}

Statisch synchronisierte Blöcke

Nicht nur Objektmethoden, sondern auch Klassenmethoden können synchronized sein. Doch die Nachbildung in einem Block sieht etwas anders aus, da es keine this-Referenz gibt. Hier kann ein Object-Exemplar für einen Lock herhalten, der extra für die Klasse angelegt wird. Dies ist eines der seltenen Beispiele, in denen ein Exemplar der Klasse Object Sinn ergibt.

Listing 11.19 com/tutego/insel/thread/StaticSync.java

package com.tutego.insel.thread; 
 
class StaticSync 
{ 
  private static final Object o = new Object(); 
 
  static void staticFoo() 
  { 
     synchronized( o ) 
     { 
       // ... 
     } 
  } 
}

Alternativ könnten wir auch das zugehörige Class-Objekt einsetzen. Wir müssen das entsprechende Klassenobjekt dann nur mittels StaticSync.class erfragen. Würden wir gleich mit Lock-Objekten arbeiten, stellt sich die Frage erst gar nicht.


Hinweis Bei Lock-Objekten oder synchronized-Blöcken kann der zwingend synchronisierbare Teil in einem kleinen Abschnitt bleiben. Die JVM kann die anderen Teile parallel abarbeiten, und andere Threads dürfen die anderen Teile betreten. Als Resultat ergibt sich eine verbesserte Geschwindigkeit.



Galileo Computing - Zum Seitenanfang

11.5.10 Dann machen wir doch gleich alles synchronisiert! Zur nächsten ÜberschriftZur vorigen Überschrift

In nebenläufigen Programmen kann es schnell zu unerwünschten Nebeneffekten kommen. Das ist auch der Grund, warum Thread-lastige Programme schwer zu debuggen sind. Warum sollten wir also nicht alle Methoden synchronisieren? Wäre dann nicht das Problem aus der Welt geschafft? Prinzipiell würde das einige Probleme lösen, doch hätten wir uns damit andere Nachteile eingefangen:

  • Methoden, die synchronisiert sind, müssen von der JVM besonders bedacht werden, damit keine zwei Threads die Methode für das gleiche Objekt ausführen. Wenn also ein zweiter Thread in die Methode eintreten möchte, kann er das nicht einfach machen, sondern muss vielleicht erst neben vielen anderen Threads warten. Es muss also eine Datenstruktur geben, in der wartende Threads eingetragen und ausgewählt werden. Das kostet zusätzlich Zeit und ist im Vergleich zu einem normalen Methodenaufruf teurer.
  • Zusätzlich kommt ein Problem hinzu, wenn eine nicht notwendigerweise, also überflüssige, synchronisierte Methode eine Endlosschleife oder lange Operationen durchführt. Dann warten alle anderen Threads auf die Freigabe, und das kann im Fall der Endlosschleife ewig sein. Auch bei Multiprozessorsystemen profitieren wir nicht von dieser Programmiertechnik. Unnötig und falsch synchronisierte Blöcke machen die Vorteile von Mehrprozessormaschinen zunichte.
  • Wenn alle Methoden synchronisiert sind, steigt auch die Gefahr eines Deadlocks. In den folgenden Abschnitten erfahren wir etwas mehr über den Fall, dass zwei Threads wechselseitig auf Ressourcen eines jeweils anderen Threads zugreifen wollen und sich dabei im Wege stehen.

Ist der gesamte Zugriff auf ein Objekt synchronisiert und kann kein zweiter Thread irgendwelche Eigenschaften parallel zu einem anderen Thread nutzen, nennt sich das Objekt voll synchronisiert im Gegensatz zu teilsynchronisiert. Sind einige Methoden der Klasse nicht synchronisiert, kann ein zweiter Thread parallel zu den synchronisierten Blöcken an die Eigenschaften gehen.


Galileo Computing - Zum Seitenanfang

11.5.11 Lock-Freigabe im Fall von Exceptions Zur nächsten ÜberschriftZur vorigen Überschrift

Kommt es innerhalb eines synchronized-Blocks beziehungsweise innerhalb einer synchronisierten Methode zu einer nicht überprüften RuntimeException, wird die JVM den Lock automatisch freigeben. Der Grund: Die Laufzeitumgebung gibt den Lock automatisch frei, wenn der Thread den synchronisierten Block verlässt, was bei einer Exception der Fall ist.

Werden die mit dem Schlüsselwort synchronized geschützten Blöcke durch Lock-Objekte umgesetzt, ist darauf zu achten, die Locks auch im Exception-Fall wieder freizugeben. Ein finally mit unlock() kommt da gerade recht, denn finally wird ja immer ausgeführt, egal, ob es einen Fehler gab oder nicht.

Listing 11.20 com/tutego/insel/thread/concurrent/UnlockInFinally.java, main()

ReentrantLock lock = new ReentrantLock(); 
 
try 
{ 
  lock.lock(); 
  System.out.println( lock.getHoldCount() ); // 1 
 
  try 
  { 
    System.out.println( 12 / 0 ); 
  } 
  finally 
  { 
    lock.unlock(); 
  } 
} 
catch ( Exception e ) 
{ 
  System.out.println( e.getMessage() );    // / by zero 
} 
System.out.println( lock.getHoldCount() ); // 0

Nach dem lock() liefert getHoldCount() eins, da ein Thread den Block betreten hat. Die Division durch null provoziert eine RuntimeException, und finally gibt den Lock frei. Die Ausnahme wird abgefangen, und getHoldCount() liefert wieder null, da finally das unlock() ausführte. Würden wir die Zeile mit unlock() auskommentieren, so würde getHoldCount() weiterhin eins liefern, was ein Fehler ist.


Galileo Computing - Zum Seitenanfang

11.5.12 Mit synchronized nachträglich synchronisieren Zur nächsten ÜberschriftZur vorigen Überschrift

Einige Java-Methoden in der Standardbibliothek sind synchronisiert, bei anderen haben die Entwickler auf eine Synchronisierung verzichtet. Nicht-synchronisierte Methoden bilden eindeutig die Mehrheit. Wenn keine ausdrücklichen Gründe für die Synchronisierung vorliegen und im Allgemeinen nur maximal ein Thread die Methode gleichzeitig aufruft, muss der Entwickler eine Absicherung nicht standardmäßig in Erwägung ziehen. Synchronisierung führt zu Geschwindigkeitsverlusten, und warum sollten wir – wenn keine Parallelität üblich ist – für etwas bezahlen, was keiner bestellt hat?

Im ersten Beispiel haben wir die Initialisierung eines Point-Objekts betrachtet. Dass der direkte Zugriff auf zwei Variablen nicht atomar sein kann, ist klar. Doch auch der Methodenaufruf über die nicht-synchronisierte Methode setLocation() bringt uns nicht weiter, weil ein Thread in dieser Methode unterbrochen werden könnte.

Wollen wir nachträglich sichergehen, dass setLocation() atomar ist, können wir über zwei Dinge nachdenken:

  • Wir verwenden ein Lock-Objekt, das allen Threads zugänglich ist. Das Objekt nutzen sie zur Synchronisation.
  • Wir besorgen uns einen Monitor auf das Point-Objekt und synchronisieren über diesen.

Die erste Variante haben wir schon gesehen, sodass wir uns ein Beispiel für die zweite Variante anschauen:

Point p = new Point(); 
synchronized( p ) 
{ 
  p.setLocation( 1, 2 ); 
}

Auf diese Weise kann jeder Aufruf einer nicht-synchronisierten Methode nachträglich synchronisiert werden. Jedoch muss dann jeder Zugriff wiederum mit einem synchronized-Block geschützt sein, sonst besteht keine Sicherheit, weil setLocation() selbst auf keinen Monitor achtet. Ruft demnach ein anderer Thread setLocation() außerhalb des synchronized-Blocks auf, ist die atomare Bearbeitung nichtig.

Wrapper

Einen anderen Weg gehen komplette Wrapper, die sich durch Delegation oder Vererbung implementieren lassen. Für unseren Punkt können wir zum Beispiel eine Unterklasse SynchronizedPoint implementieren, die alle kritischen Methoden überschreibt, die Signatur mit synchronized erweitert und im Rumpf ein super.methode() durchführt. Wrapper gibt es für Datenstrukturen aus der Collection-API mit statischen Methoden der Art synchronized-Datenstruktur(Datenstruktur) – etwa für Listen:

List list = Collections.synchronizedList( myList );

Galileo Computing - Zum Seitenanfang

11.5.13 Monitore sind reentrant – gut für die Geschwindigkeit Zur nächsten ÜberschriftZur vorigen Überschrift

Betritt das Programm eine synchronisierte Methode, bekommt es den Monitor des aufrufenden Objekts. Wenn diese Methode eine andere aufruft, die am gleichen Objekt synchronisiert ist, kann sie sofort eintreten und muss nicht warten. Diese Eigenschaft heißt reentrant. Ohne diese Möglichkeit würde Rekursion nicht funktionieren!

Wenn das Programm den synchronisierten Block betritt, reserviert er den Monitor und kann alle synchronisierten Methoden ohne weitere Überprüfungen ausführen. Im Allgemeinen reduziert diese Technik aber auch die Parallelität, da der kritische Abschnitt künstlich vergrößert wird. Die Technik kann geschwindigkeitssteigernd sein, wenn viele synchronisierte Methoden hintereinander aufgerufen werden.


Beispiel In StringBuffer sind viele Methoden synchronisiert. Dies bedeutet, dass bei jedem Aufruf einer Methode der Monitor reserviert werden muss. Das kostet natürlich eine Kleinigkeit, und als Lösung bietet es sich an, die Aufrufe in einem eigenen synchronisierten Block zu bündeln.

StringBuffer sb = new StringBuffer(); 
synchronized( sb ) 
{ 
  sb.append( "Transpirations-" ); 
  sb.append( "Illustration" ); 
  sb.append( "\t" ); 
  sb.append( "Röstreizstoffe" ); 
}

Wir können uns vorstellen, dass bei der Klasse ein kleiner Zähler ist, der bei jedem Betreten inkrementiert und beim Verlassen dekrementiert wird. Ist der Zähler null, befindet sich kein Thread im Block. Ist er größer null, haben wir einen reentranten Zugriff.


Die Klasse ReentrantLock verwaltet den Zähler – er geht bis 231 – selbst, und einige Methoden geben Zugriff auf die Informationen, die meistens zum Testen nützlich sind. Mit isLocked() finden wir heraus, ob der Lock frei ist oder nicht. isHeldByCurrentThread() liefert true, wenn der ausführende Thread den Lock verwendet. getHoldCount() liefert die Anzahl der Anfragen, die der aktuelle Thread an den Lock gestellt hat. Ist die Rückgabe null, so schließt der aktuelle Thread nicht ab, doch könnte dies wohl ein anderer Thread erledigen. getQueueLength() gibt eine (durch race conditions mögliche) Schätzung über die Anzahl der wartenden Threads ab, die lock() aufgerufen haben.


Galileo Computing - Zum Seitenanfang

11.5.14 Synchronisierte Methodenaufrufe zusammenfassen Zur nächsten ÜberschriftZur vorigen Überschrift

Synchronisierte Methoden stellen sicher, dass bei mehreren parallel ausführenden Threads die Operationen atomar ausgeführt werden. Das gilt jedoch ausschließlich für jede synchronisierte Methode, aber nicht für eine Sequenz von synchronisierten Methoden. Wir wissen zum Beispiel, dass StringBuffer alle Methoden synchronisiert und daher der StringBuffer bei parallelen Zugriffen keine inkonsistenten Zustände erzeugt. Was geschieht, wenn zwei Threads auf den folgenden Block zugreifen, wobei sb eine Variable ist, die auf einen gemeinsamen StringBuffer zeigt?

for ( char c = 'a'; c <= 'z'; c++ ) 
  sb.append( c );

Greifen zwei Threads – nennen wir sie T1 und T2 – auf sb zu, erzeugen möglicherweise beide zusammen die folgende Zeichenkette: abcabcdefgdhihij... Das Ergebnis ist logisch, denn synchronized bedeutet nur, dass zwei Threads eine einzelne Operation atomar ausführen, aber kein Bündel.

Diese Aufgabe löst ein synchronisierter Block ausgezeichnet.

synchronized ( sb ) 
{ 
  for ( char c = 'a'; c <= 'z'; c++ ) 
    sb.append( c ); 
}

Betritt der erste Thread den synchronisierten Block, schließt er ab, sodass andere Threads warten müssen. Der betretende Thread selbst kann aber, weil er den Monitor des StringBuffer schon besitzt, reentrant die anderen synchronisierten Methoden aufrufen.

Das Beispiel zeigt, wie gut sich ein synchronized-Block nutzen lässt, wenn an anderen Objekten synchronisiert wird. Mit einem Lock-Objekt könnten wir hier nicht arbeiten, weil es zwar die einzelnen append()-Aufrufe zusammenfasst, aber von außen eine Unterbrechung nicht verhindern kann. Wenn ein zweiter Thread sich in die Aufrufkette mogelt, kann er jedes Mal, wenn ein append() verlassen und dabei der Monitor frei wird, ein neues append() aufrufen und so außerhalb des lock()/unlock()-Blocks eintreten.


Galileo Computing - Zum Seitenanfang

11.5.15 Deadlocks topZur vorigen Überschrift

Ein Deadlock (zu Deutsch etwa »tödliche Umarmung«) kommt beispielsweise dann vor, wenn ein Thread A eine Ressource belegt, die ein anderer Thread B haben möchte. Dieser Thread B belegt aber eine Ressource, die A gerne bekommen würde. In dieser Situation können beide nicht vor und zurück und befinden sich in einem dauernden Wartezustand. Deadlocks können in Java-Programmen nicht erkannt und verhindert werden. Uns fällt also die Aufgabe zu, diesen ungünstigen Zustand gar nicht erst herbeizuführen.

Das nächste Beispiel soll über eine Verklemmung einen Deadlock provozieren. Zwei Threads wetteifern um die Objekte Locks alock1 und lock2b. Dabei kommt es zu einem Deadlock, da der eine genau den einen Lock besetzt, den der jeweils andere zum Weiterarbeiten benötigt.

Listing 11.21 com/tutego/insel/thread/Deadlock.java

package com.tutego.insel.thread; 
 
import java.util.concurrent.TimeUnit; 
import java.util.concurrent.locks.*; 
 
class Deadlock 
{ 
  static Lock lock1 = new ReentrantLock(), 
              lock2 = new ReentrantLock(); 
 
  static class T1 extends Thread 
  { 
    @Override 
    public void run() 
    { 
      lock1.lock(); 
      System.out.println( "T1: Lock auf lock1 bekommen" ); 
 
      try { TimeUnit.SECONDS.sleep( 1 ); } catch ( InterruptedException e ) { } 
 
      lock2.lock(); 
      System.out.println( "T1: Lock auf lock2 bekommen" ); 
 
      lock2.unlock(); 
      lock1.unlock(); 
    } 
  } 
 
  static class T2 extends Thread 
  { 
    @Override 
    public void run() 
    { 
      lock2.lock(); 
      System.out.println( "T2: Lock auf lock2 bekommen" ); 
 
      lock1.lock(); 
      System.out.println( "T2: Lock auf lock1 bekommen" ); 
 
      lock1.unlock(); 
      lock2.unlock(); 
    } 
  } 
 
  public static void main( String[] args ) 
  { 
    new T1().start(); 
    new T2().start(); 
  } 
}

In der Ausgabe sehen wir nur zwei Zeilen, und dann hängt das gesamte Programm:

T1: Lock auf lock1 bekommen 
T2: Lock auf lock2 bekommen

Eine Lösung des Problems wäre, bei geschachteltem Synchronisieren auf mehrere Objekte diese immer in der gleichen Reihenfolge zu belegen, also etwa immer erst lock1, dann lock2. Bei unbekannten, dynamisch wechselnden Objekten muss dann unter Umständen eine willkürliche Ordnung festgelegt werden.


Hinweis Die Sun JVM verfügt über eine eingebaute Deadlock-Erkennung, die auf der Konsole aktiviert werden kann. Dazu ist unter Windows die Tastenkombination Strg + Break zu drücken und unter Linux oder Solaris Strg + \.




Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.






<< zurück
  Zum Katalog
Zum Katalog: Java ist auch eine Insel





Java ist auch eine Insel
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Tipp
Zum Katalog: Coding for Fun





 Coding for Fun


 Buchempfehlungen
Zum Katalog: Objektorientierte Programmierung





 Objektorientierte
 Programmierung


Zum Katalog: Einstieg in Eclipse 3.4






 Einstieg in
 Eclipse 3.4


Zum Katalog: Java 6 lernen mit Eclipse






 Java 6 lernen
 mit Eclipse


Zum Katalog: NetBeans Platform 6






 NetBeans
 Platform 6


Zum Katalog: Java und XML






 Java und XML


Zum Katalog: Visual C# 2008






 Visual C# 2008


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


Zum Katalog: C++ von A bis Z






 C++ von A bis Z


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Galileo Press 2009
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de