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

Inhaltsverzeichnis
Vorwort
1 Neues in Java 7
2 Threads und nebenläufige Programmierung
3 Datenstrukturen und Algorithmen
4 Raum und Zeit
5 Dateien, Verzeichnisse und Dateizugriffe
6 Datenströme
7 Die eXtensible Markup Language (XML)
8 Dateiformate
9 Grafische Oberflächen mit Swing
10 Grafikprogrammierung
11 Netzwerkprogrammierung
12 Verteilte Programmierung mit RMI
13 RESTful und SOAP Web-Services
14 JavaServer Pages und Servlets
15 Applets
16 Datenbankmanagement mit JDBC
17 Technologien für die Infrastruktur
18 Reflection und Annotationen
19 Dynamische Übersetzung und Skriptsprachen
20 Logging und Monitoring
21 Java Native Interface (JNI)
22 Sicherheitskonzepte
23 Dienstprogramme für die Java-Umgebung
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
Java 7 - Mehr als eine Insel von Christian Ullenboom
Das Handbuch zu den Java SE-Bibliotheken
Buch: Java 7 - Mehr als eine Insel

Java 7 - Mehr als eine Insel
Rheinwerk Computing
1433 S., 2012, geb.
49,90 Euro, ISBN 978-3-8362-1507-7
Pfeil 2 Threads und nebenläufige Programmierung
Pfeil 2.1 Threads erzeugen
Pfeil 2.1.1 Threads über die Schnittstelle Runnable implementieren
Pfeil 2.1.2 Thread mit Runnable starten
Pfeil 2.1.3 Die Klasse Thread erweitern
Pfeil 2.2 Thread-Eigenschaften und -Zustände
Pfeil 2.2.1 Der Name eines Threads
Pfeil 2.2.2 Wer bin ich?
Pfeil 2.2.3 Die Zustände eines Threads *
Pfeil 2.2.4 Schläfer gesucht
Pfeil 2.2.5 Mit yield() auf Rechenzeit verzichten
Pfeil 2.2.6 Der Thread als Dämon
Pfeil 2.2.7 Das Ende eines Threads
Pfeil 2.2.8 Einen Thread höflich mit Interrupt beenden
Pfeil 2.2.9 UncaughtExceptionHandler für unbehandelte Ausnahmen
Pfeil 2.2.10 Der stop() von außen und die Rettung mit ThreadDeath *
Pfeil 2.2.11 Ein Rendezvous mit join() *
Pfeil 2.2.12 Arbeit niederlegen und wieder aufnehmen *
Pfeil 2.2.13 Priorität *
Pfeil 2.3 Der Ausführer (Executor) kommt
Pfeil 2.3.1 Die Schnittstelle Executor
Pfeil 2.3.2 Die Thread-Pools
Pfeil 2.3.3 Threads mit Rückgabe über Callable
Pfeil 2.3.4 Mehrere Callable abarbeiten
Pfeil 2.3.5 ScheduledExecutorService für wiederholende Ausgaben und Zeitsteuerungen nutzen
Pfeil 2.4 Synchronisation über kritische Abschnitte
Pfeil 2.4.1 Gemeinsam genutzte Daten
Pfeil 2.4.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
Pfeil 2.4.3 Punkte parallel initialisieren
Pfeil 2.4.4 i++ sieht atomar aus, ist es aber nicht *
Pfeil 2.4.5 Kritische Abschnitte schützen
Pfeil 2.4.6 Kritische Abschnitte mit ReentrantLock schützen
Pfeil 2.4.7 Synchronisieren mit synchronized
Pfeil 2.4.8 Synchronized-Methoden der Klasse StringBuffer *
Pfeil 2.4.9 Mit synchronized synchronisierte Blöcke
Pfeil 2.4.10 Dann machen wir doch gleich alles synchronisiert!
Pfeil 2.4.11 Lock-Freigabe im Fall von Exceptions
Pfeil 2.4.12 Deadlocks
Pfeil 2.4.13 Mit synchronized nachträglich synchronisieren *
Pfeil 2.4.14 Monitore sind reentrant – gut für die Geschwindigkeit *
Pfeil 2.4.15 Synchronisierte Methodenaufrufe zusammenfassen *
Pfeil 2.5 Synchronisation über Warten und Benachrichtigen
Pfeil 2.5.1 Die Schnittstelle Condition
Pfeil 2.5.2 It’s Disco-Time *
Pfeil 2.5.3 Warten mit wait() und Aufwecken mit notify() *
Pfeil 2.5.4 Falls der Lock fehlt: IllegalMonitorStateException *
Pfeil 2.6 Datensynchronisation durch besondere Concurrency-Klassen *
Pfeil 2.6.1 Semaphor
Pfeil 2.6.2 Barrier und Austausch
Pfeil 2.6.3 Stop and go mit Exchanger
Pfeil 2.7 Atomare Operationen und frische Werte mit volatile *
Pfeil 2.7.1 Der Modifizierer volatile bei Objekt-/Klassenvariablen
Pfeil 2.7.2 Das Paket java.util.concurrent.atomic
Pfeil 2.8 Teile und herrsche mit Fork und Join *
Pfeil 2.8.1 Algorithmendesign per »teile und herrsche«
Pfeil 2.8.2 Paralleles Lösen von D&C-Algorithmen
Pfeil 2.8.3 Fork und Join
Pfeil 2.9 Mit dem Thread verbundene Variablen *
Pfeil 2.9.1 ThreadLocal
Pfeil 2.9.2 InheritableThreadLocal
Pfeil 2.9.3 ThreadLocalRandom als Zufallszahlengenerator
Pfeil 2.9.4 ThreadLocal bei der Performance-Optimierung
Pfeil 2.10 Threads in einer Thread-Gruppe *
Pfeil 2.10.1 Aktive Threads in der Umgebung
Pfeil 2.10.2 Etwas über die aktuelle Thread-Gruppe herausfinden
Pfeil 2.10.3 Threads in einer Thread-Gruppe anlegen
Pfeil 2.10.4 Methoden von Thread und ThreadGroup im Vergleich
Pfeil 2.11 Zeitgesteuerte Abläufe
Pfeil 2.11.1 Die Typen Timer und TimerTask
Pfeil 2.11.2 Job-Scheduler Quartz
Pfeil 2.12 Einen Abbruch der virtuellen Maschine erkennen
Pfeil 2.13 Zum Weiterlesen

Rheinwerk Computing - Zum Seitenanfang

2.8 Teile und herrsche mit Fork und Join *Zur nächsten Überschrift


Rheinwerk Computing - Zum Seitenanfang

2.8.1 Algorithmendesign per »teile und herrsche«Zur nächsten ÜberschriftZur vorigen Überschrift

Eine effektive Problemlösungsstrategie ist es, zunächst das Problem in Teilprobleme zu zerlegen, dann die Teilprobleme zu lösen und anschließend zur Gesamtlösung zu kommen. Wer morgens im Bett liegt und Hunger verspürt, der wird erst dann satt sein, wenn gewisse Teilprobleme gelöst sind. Hierarchisch kann das etwa so aussehen:

Tabelle 2.7: Sequentielle und parallelisierbare Aufgaben beim Aufstehen

Der Morgen

Aufstehen

Augen auf

Räkeln

Aus dem Bett steigen/fallen

Essen

Kühlschrank aufmachen

Essen entnehmen

Essen zubereiten

Essen aufnehmen

Diese Problemlösungsstrategie wird Teile und herrsche (engl. divide and conquer, D&C) genannt. Zunächst wird die Aufgabe ist kleine Häppchen zerlegt und anschließend abgearbeitet.

Teile und herrsche ist nicht nur eine Lösung, wie wir eine große Pizza »verarbeiten«, sondern auch in der Informatik eine beliebte algorithmische Methode: Das Hauptproblem wird in Teilprobleme zerlegt, und dann werden die Teilprobleme gelöst und zur großen Lösung zusammengefügt. Zwei populäre Beispiele sind Sortierungen und die Multiplikation von großen Zahlen.

Sortieren über das Merge-Sort-Verfahren

Der von John von Neumann vorgestellte Algorithmus basiert auf der Idee, die zu sortierende Liste ein zwei Teillisten zu zerlegen, diese dann wiederum in zwei Teile zu zerlegen, diese wiederum usw., bis die Listen so klein sind, dass sie vielleicht nur noch aus zwei Zahlen bestehen, die trivial in eine Reihenfolge zu bringen sind. Ist eine Teilfolge dann sortiert, muss sie mit der sortieren Nachbarfolge zusammenfügt (engl. merge) werden. Während also das Zerlegen und Sortieren von oben nach unten erfolgt, läuft das Zusammenlegen der sortierten Teillisten zur neuen größeren sortierten Teillisten von unten nach oben, bis schließlich die Gesamtliste sortiert ist. Der Algorithmus lässt sich sehr gut rekursiv implementieren. Auch das bekannte Quicksort arbeitet ähnlich. (Hier geht es allerdings darum, ein sogenanntes Pivot-Element zu wählen, dann die Liste in zwei Teillisten aufzuspalten, wobei in die erste Liste (erst einmal unsortiert) die Elemente kleiner dem Pivot-Element verschoben werden und in die andere Liste die Elemente größer dem Pivot-Element. Die Auswahl eines neuen Pivot-Elements und das Kopieren in den richtigen Bereich wird rekursiv für die Unterbereiche wiederholt, was natürlich zu einer Sortierung führt. In der Regel kommt Quicksort mit weniger Speicher aus und ist in der Praxis schneller, da Merge-Sort in der einfachen Implementierung immer neue Teillisten aufbauen muss und Quicksort die Vertauschoperationen auf der originalen Datenstruktur (also in-place) ausführen kann.

Multiplikation von großen Ganzzahlen

In Java ist das Multiplizieren von Ganzahlen einfach. Sind die Zahlen klein genug, erledigt der *-Operator die Aufgabe, sind sie größer, helfen die Klasse BigInteger und die Methode multiply(). Das sind natürlich hübsche Abstraktionen, aber im Java-Bytecode gibt es für die Multiplikation von int und long lediglich imul und lmul,[11](http://java.sun.com/docs/books/jvms/second_edition/html/Mnemonics.doc.html) und alles andere, etwa die Multiplikation von großen Zahlen für RSA-Schlüssel, müssen wir anders lösen.

Das Produkt von großen Zahlen lässt sich einfach mit ein paar Additionen auf das Produkt von kleineren Zahlen abbilden. Anstatt Zahlen mit Hunderten von Stellen zu nehmen, nutzen wir ein einfacheres Beispiel, das das Prinzip zeigt. Nehmen wir dazu die Zahl A = 1234, die mit B = 5678 multipliziert werden soll. Dann ist AB = (12 · 10^2 + 34) · (56 · 10^2 + 78) = 12 · 56 · 10^4 + (12 · 78 + 34 · 56) · 10^2 + 34 · 78. Waren bei 1234 und 5678 die Zahlen noch vierstellig, sind sie bei der Umschreibung nur noch zweistellig. Zählen wir die Anzahl der Multiplikationen – und lassen wir die einfachen Multiplikation mit 10^4 bzw. 10^2 beiseite –, so kommen wir auf vier, denn wir müssen 12 · 56, 12 · 78, 34 · 56 und 34 · 78 ausführen. Bei einem rekursiven D&C-Algorithmus ist also das Problem zur Multiplikation von 1234 · 5678 auf die vier Multiplikationen und Additionen abgeschwächt worden. Das können wir dann auch weiter aufspalten, bis wir bei einstelligen Zahlen sind.

Stehen wir also vor der Aufgabe, beliebig große Zahlen mit n Stellen zur multiplizieren, können wir das auf eine Multiplikation von Zahlen der Größe n/2 und ein paar Additionen abbilden. Kommen wir noch zu einer kleinen Optimierung. Wenn Zahlen sehr groß werden und dann multipliziert werden müssen (etwa zu Schlüsselgenerierung) ist es wichtig, jede überflüssige Operation wegzulassen, da arithmetische Operationen dann bei großen Zahlen und häufiger Durchführung doch ihre Zeit brauchen. Interessanterweise kann durch geschickte Umstellung die Anzahl der Multiplikationen von 4 auf 3 gesenkt werden. Zwei der Multiplikationen aus 12 · 56 · 10^4 + (12 · 78 + 34 · 56) · 10^2 + 34 · 78 stammen aus dem Teil 12 · 78 + 34 · 56. Hier können wir etwas umschreiben, denn 12 · 78 + 34 · 56 = (12 + 34) · (56 + 78) – 12 · 56 – 34 · 78. Obwohl das auf den ersten Blick schlimmer aussieht (drei Multiplikationen statt zwei), fällt bei einem zweiten Blick auf, dass wir die beiden Produkte 12 · 56 und 34 · 78 schon im ersten Schritt berechnet haben. Also ergibt sich letztendlich 12 · 56 · 10^4 + ((12 + 34) · (56 + 78)12 · 5634 · 78) · 10^2 + 34 · 78, und das macht insgesamt drei Multiplikationen für den Preis von ein paar zusätzlichen Subtraktionen, die im Allgemeinen billiger sind als die Multiplikationen, die bei dem D&C-Ansatz ja recht aufwändig sind.

Die Arbeitsweise von D&C-Algorithmen sieht im Pseudocode wie folgt aus:

löse Problem:
ist Problem klein:
löse Problem direkt
andernfalls:
zerlege das Problem in Teilprobleme
löse die Teilprobleme
setze Problemlösung aus den Teillösungen zusammen

Attraktiv sind D&C-Algorithmen dann, wenn die Teilprobleme unabhängig voneinander und parallel gelöst werden können.

Bei unserem Eingangsbeispiel mit dem Aufstehen und Essen gibt es eine Abhängigkeit, sodass zwei beide Teilprozesse zwar eine Teilaufgabe des Gesamtproblems lösen, man aber ohne aufzustehen nicht zum Kühlschrank kommt. Das Sortieren über Merge-Sort erfüllt dabei das Kriterium, dass wenn die Liste in zwei Unterlisten zerlegt wird, die beiden Unterlisten problemlos parallel sortiert werden können.


Rheinwerk Computing - Zum Seitenanfang

2.8.2 Paralleles Lösen von D&C-AlgorithmenZur nächsten ÜberschriftZur vorigen Überschrift

Die Abarbeitung von Teilproblemen durch Threads ergibt immer dann Sinn, wenn

  • durch die Kommunikation mit externen Subsystemen einzelne Threads den Prozessor immer wieder durch Wartezeiten nicht vollständig ausnutzen oder
  • Threads effektiv parallel auf mehreren Prozessoren oder Cores laufen.

Kommen wir kurz zum Pseudo-Code vom D&C-Algorithmus zurück. Die Parallelisierung ist an einer Stelle:

löse Problem:
ist Problem klein:
löse Problem direkt
andernfalls:
zerlege das Problem in Teilprobleme
löse die Teilprobleme parallel
warte auf die Fertigstellung
setze Problemlösung aus den Teillösungen zusammen

Die Teilprobleme könnten parallel gelöst werden, und es muss keine Rekursion stattfinden.

Mit den Standardmitteln von Java 1.0 könnten wir eine parallele D&C-Lösung prinzipiell implementieren. Wir bauen mit new Thread() einen Thread auf, geben die zu lösende Aufgabe als Runnable mit, starten mit start() den Thread und warten anschließend mit join() auf das Ende. Anschließend bringen wir die Ergebnisse zusammen.

Ein genauer Blick auf diese Lösung zeigt ein zentrales Problem auf: Es werden viele Threads benötigt. Nehmen wir eine Problemgröße von n an. Im ersten Schritt werden zwei Threads benötigt, die jeweils die Probleme der Größe n/2 und n/2 lösen. Für diese würden wieder Threads benötigt, diesmal 4, die dann die Problemgrößen n/4, n/4, n/4 und n/4 lösen. Da sich die Problemgröße immer halbiert, ergibt sich ein Binärbaum der Tiefe log2n. Die Anzahl der Threads ist 2 + 4 + 8 + 16 + ... + 2log2n = n, also abhängig von der Problemgröße und in der Größenordnung O(n). Wir könnten nun argumentieren, dass Problem mit den Thread-Pool abzumildern, aber im Grunde werden immer noch zu viele Threads benötigt. Und wenn am Anfang durch die erste Teilung für die Problemgröße n/2 zwei Threads erzeugt werden, was machen sie? Zunächst nichts als warten. Sie warten auf die Berechnung der aufgespannten Threads, die die zwei n/4-Lösung liefern. Diese warten wiederum bis zum Boden, bis die Lösung wirklich so klein ist, dass sie direkt berechnet werden kann. Erst dann läuft das Ergebnis wieder nach oben, und Schritt für Schritt beendet das die oberen wartenden Threads.

Im Grunde warten die Threads mehr, als sie arbeiten. Daher bringt auch ein Thread-Pool keine unglaubliche Verbesserung, denn wartende Threads können vom Thread-Pool nicht für andere Aufgaben herangezogen werden, sondern der Thread-Pool muss neue Threads aufbauen. Wenn wir dann fordern, dass der Thread-Pool nur so groß ist wie die Anzahl an Prozessoren (etwa 2), dann ist klar, dass wir sofort in einer Sackgasse stecken würden. Traditionelle Thread-Pools helfen bei der Lösung von parallelen D&C-Ansätzen so einfach nicht.


Rheinwerk Computing - Zum Seitenanfang

2.8.3 Fork und JoinZur vorigen Überschrift

Gesucht ist ein Framework zum Lösen von parallelen D&C-Algorithmen, die berechnungsintensiv sind. Oracle hat in Java 7 das Framework Fork/Join integriert, was im Rahmen von jsr166y (http://gee.cs.oswego.edu/dl/concurrency-interest/) unter maßgeblicher Arbeit von Doug Lea entwickelt wurde. Die grundlegende Idee ist, neben Threads noch eine andere Arbeitseinheit einzuführen, die Tasks.

  • Threads: Werden vom Betriebssystem verwaltet und laufen entweder pseudo-parallel auf einem Prozessor/Core oder echt parallel. Threads können sich mit anderen Threads koordinieren. Zu viele Threads, die sich im Weg stehen und aufeinander warten, führen zu keiner verbesserten Ausführungszeit gegenüber einer sequenziellen Lösung.
  • Tasks: Werden von Threads bzw. einem Thread-Pool ausgeführt. Sie sind Arbeitseinheiten, die nicht auf andere Tasks warten.

Die Tasks sind kleine Arbeitspakete und werden in eine Task-Queue gelegt und dann von Threads abgearbeitet. Hat das System zwei Prozessoren und hat der Thread-Pool die Größe 2, so ist es wahrscheinlich, dass 2 Tasks parallel abgearbeitet werden. Gibt es 4 Prozessoren, können 4 Tasks vielleicht parallel laufen. Tasks lassen sich also grundsätzlich auf einer beliebige Anzahl von Threads und somit Prozessoren/Cores bringen, wobei im Gegensatz die Effektivität von Threads immer mit der physikalischen Anzahl von Prozessoren/Cores assoziiert ist.

Das Fork/Join-Framework aus dem java.util.concurrent-Paket löst die Probleme effektiv. Wie der Name schon andeutet, geht es bei Fork um das Erstellen eines neuen Tasks und bei Join um das Zusammenführen der Ergebnisse. Die Fork/Join-Bibliothek stellt dazu die Klasse ForkJoinPool als Koordinator zur Verfügung und als Task-Beschreibung den Typ ForkJoinTask. Von der abstrakten Basisklasse ForkJoinTask gibt es bisher zwei Unterklassen: RecursiveAction (Tasks ohne Ergebnisse) und RecursiveTask (Tasks mit Ergebnissen). Die zentralen Methoden sind fork() und join().

Zur Abarbeitung der Tasks stellt das Framework die Threads zur Verfügung, deren Anzahl wir zwar für die Lösung des Problems selbst bestimmen können, aber die Anzahl der Prozessoren/Cores ist eine gute Standardgröße.[12](Dass die Maximalanzahl von Threads beim ForkJoinPool zurzeit 32767 ist, dürfte für normale Nutzer keine Einschränkung sein.) Die Methode fork() erzeugt einen neuen Task, der an den Anfang (!) einer Queue gestellt wird. Dabei haben alle Threads eine Queue für ihre Arbeitsaufträge, und sollte einmal eine Queue leergelaufen sein, so nimmt sich der Thread einfach einen Task vom Ende (!) einer anderen nicht-leeren Queue. (Das nennt sich work-stealing und ist im wahren Leben ziemlich selten anzutreffen.) Dass neue Tasks an den Anfang gestellt werden, ist einfach zu erklären: Die Tasks werden ja immer kleiner, und somit stehen die kleinen, schnell lösbaren Aufgaben vorne. Erst später folgen die größeren Aufgaben, die auf die Ergebnisse der kleinen Aufgaben zurückgreifen, die dann logischerweise schon berechnet wurden.

Zur Theorie ein Beispiel: Es geht darum, mit Fork/Join ein Programm zu haben, das parallel das Maximum eines Arrays sucht. Der Start ist:

Listing 2.37: com/tutego/insel/thread/concurrent/ForkJoinPoolDemo,java, main()

int[] array = { 0, 9, 10, 111, 1, 12, 13, 14, 17 };
System.out.println( MaxElementInArrayFinder.findMax( array ) );

Die eigene Klasse MaxElementInArrayFinder bietet die Methode findMax(), die auf den ForkJoinPool zurückgreift, um mit invoke() den Haupt-Task abzusetzen.

Listing 2.38: com/tutego/insel/thread/concurrent/ForkJoinPoolDemo,java, MaxElementInArrayFinder

class MaxElementInArrayFinder
{
private static final ForkJoinPool fjPool = new ForkJoinPool();
...
public static int findMax( int[] array )
{
return fjPool.invoke( new MaxElemTask( array, 0, array.length –1 ) );
}
}

Unsere Klasse MaxElemTask repräsentiert ein Arbeitspakt. Das Objekt referenziert jeweils das Array sowie die Anfangs-/Endeposition, ab der sie nach dem Maximum suchen sollen.

Listing 2.39: com/tutego/insel/thread/concurrent/ForkJoinPoolDemo,java, MaxElementInArrayFinder.MaxElemTask

private static class MaxElemTask extends RecursiveTask<Integer>
{
private final int[] array;
private final int start, end;

MaxElemTask( int[] array, int start, int end )
{
assert array != null && start >= 0 && start <= end;

this.array = array;
this.start = start;
this.end = end;
}

@Override protected Integer compute()
{
...
}
}

Unsere Klasse erweitert die Basisklasse RecursiveTask<Integer> und deutet durch den generischen Typ schon an, dass das Ergebnis des Tasks eine Ganzzahl sein wird, nämlich das Feldmaximum aus dem gewünschten Bereich. Der Konstruktor sichert die Werte, und compute() führt die eigentliche Arbeit aus: Es löst entweder das Problem direkt, wenn es klein genug ist, oder spannt Unter-Tasks auf und wartet anschließend auf deren Ergebnisse.

Listing 2.40: com/tutego/insel/thread/concurrent/ForkJoinPoolDemo,java, MaxElementInArrayFinder.MaxElemTask.compute()

@Override protected Integer compute()
{
assert array != null && array.length > 0;

System.out.printf( "max( start=%d, end=%d )%n", start, end );

if ( end – start < 4 )
{
int max = array[start];
for ( int i = start + 1; i <= end; i++ )
if ( array[i] > max )
max = array[i];

return max;
}

int middle = (start + end) / 2;

MaxElemTask leftTask = new MaxElemTask( array, start, middle );
leftTask.fork();

MaxElemTask rightTask = new MaxElemTask( array, middle + 1, end );

int rightMax = rightTask.compute();
int leftMax = leftTask.join();

return Math.max( rightMax, leftMax );
}
Hinweis

Fork/Join ist am besten für rechenzeitintensive Programme geeignet. Gibt es Wartezeiten auf Ressourcen, dann ist der klassische Executor noch besser.



Ihr Kommentar

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

>> Zum Feedback-Formular
<< zurück
  Zum Katalog
Neuauflage: Java SE 8 Standard-Bibliothek
Neuauflage: Java SE 8 Standard-Bibliothek
Jetzt bestellen


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

 Buchempfehlungen
Zum Katalog: Professionell entwickeln mit Java EE 7






 Professionell
 entwickeln mit
 Java EE 7


Zum Katalog: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Katalog: Einstieg in Eclipse






 Einstieg in Eclipse


Zum Katalog: Einstieg in Java






 Einstieg in Java


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2012
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das 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.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de