Java SE 8 Standard-Bibliothek  
Professionelle Bücher. Auch für Einsteiger.
 
Inhaltsverzeichnis


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

Jetzt Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Java SE 8 Standard-Bibliothek von Christian Ullenboom
Das Handbuch für Java-Entwickler
Buch: Java SE 8 Standard-Bibliothek

Java SE 8 Standard-Bibliothek
Pfeil 3 Threads und nebenläufige Programmierung
Pfeil 3.1 Threads erzeugen
Pfeil 3.1.1 Threads über die Schnittstelle Runnable implementieren
Pfeil 3.1.2 Thread mit Runnable starten
Pfeil 3.1.3 Die Klasse Thread erweitern
Pfeil 3.2 Thread-Eigenschaften und -Zustände
Pfeil 3.2.1 Der Name eines Threads
Pfeil 3.2.2 Wer bin ich?
Pfeil 3.2.3 Die Zustände eines Threads *
Pfeil 3.2.4 Schläfer gesucht
Pfeil 3.2.5 Mit yield() auf Rechenzeit verzichten
Pfeil 3.2.6 Der Thread als Dämon
Pfeil 3.2.7 Freiheit für den Thread – das Ende
Pfeil 3.2.8 Einen Thread höflich mit Interrupt beenden
Pfeil 3.2.9 UncaughtExceptionHandler für unbehandelte Ausnahmen
Pfeil 3.2.10 Der stop() von außen und die Rettung mit ThreadDeath *
Pfeil 3.2.11 Ein Rendezvous mit join(…) *
Pfeil 3.2.12 Arbeit niederlegen und wieder aufnehmen *
Pfeil 3.2.13 Priorität *
Pfeil 3.3 Der Ausführer (Executor) kommt
Pfeil 3.3.1 Die Schnittstelle Executor
Pfeil 3.3.2 Glücklich in der Gruppe – die Thread-Pools
Pfeil 3.3.3 Threads mit Rückgabe über Callable
Pfeil 3.3.4 Mehrere Callable abarbeiten
Pfeil 3.3.5 ScheduledExecutorService für wiederholende Ausgaben und Zeitsteuerungen nutzen
Pfeil 3.4 Synchronisation über kritische Abschnitte
Pfeil 3.4.1 Gemeinsam genutzte Daten
Pfeil 3.4.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
Pfeil 3.4.3 Punkte nebenläufig initialisieren
Pfeil 3.4.4 i++ sieht atomar aus, ist es aber nicht *
Pfeil 3.4.5 Kritische Abschnitte schützen
Pfeil 3.4.6 Kritische Abschnitte mit ReentrantLock schützen
Pfeil 3.4.7 Synchronisieren mit synchronized
Pfeil 3.4.8 Synchronized-Methoden der Klasse StringBuffer *
Pfeil 3.4.9 Mit synchronized synchronisierte Blöcke
Pfeil 3.4.10 Dann machen wir doch gleich alles synchronisiert!
Pfeil 3.4.11 Lock-Freigabe im Fall von Exceptions
Pfeil 3.4.12 Deadlocks
Pfeil 3.4.13 Mit synchronized nachträglich synchronisieren *
Pfeil 3.4.14 Monitore sind reentrant – gut für die Geschwindigkeit *
Pfeil 3.4.15 Synchronisierte Methodenaufrufe zusammenfassen *
Pfeil 3.5 Synchronisation über Warten und Benachrichtigen
Pfeil 3.5.1 Die Schnittstelle Condition
Pfeil 3.5.2 It’s Disco-Time *
Pfeil 3.5.3 Warten mit wait(…) und Aufwecken mit notify()/notifyAll() *
Pfeil 3.5.4 Falls der Lock fehlt – IllegalMonitorStateException *
Pfeil 3.6 Datensynchronisation durch besondere Concurrency-Klassen *
Pfeil 3.6.1 Semaphor
Pfeil 3.6.2 Barrier und Austausch
Pfeil 3.6.3 Stop and go mit Exchanger
Pfeil 3.7 Atomare Operationen und frische Werte mit volatile *
Pfeil 3.7.1 Der Modifizierer volatile bei Objekt-/Klassenvariablen
Pfeil 3.7.2 Das Paket java.util.concurrent.atomic
Pfeil 3.8 Teile und herrsche mit Fork und Join *
Pfeil 3.8.1 Algorithmendesign per »teile und herrsche«
Pfeil 3.8.2 Nebenläufiges Lösen von D&C-Algorithmen
Pfeil 3.8.3 Fork und Join
Pfeil 3.9 CompletionStage und CompletableFuture *
Pfeil 3.10 Mit dem Thread verbundene Variablen *
Pfeil 3.10.1 ThreadLocal
Pfeil 3.10.2 InheritableThreadLocal
Pfeil 3.10.3 ThreadLocalRandom als schneller nebenläufiger Zufallszahlengenerator
Pfeil 3.10.4 ThreadLocal bei der Performance-Optimierung
Pfeil 3.11 Threads in einer Thread-Gruppe *
Pfeil 3.11.1 Aktive Threads in der Umgebung
Pfeil 3.11.2 Etwas über die aktuelle Thread-Gruppe herausfinden
Pfeil 3.11.3 Threads in einer Thread-Gruppe anlegen
Pfeil 3.11.4 Methoden von Thread und ThreadGroup im Vergleich
Pfeil 3.12 Zeitgesteuerte Abläufe
Pfeil 3.12.1 Die Typen Timer und TimerTask
Pfeil 3.12.2 Job-Scheduler Quartz
Pfeil 3.13 Einen Abbruch der virtuellen Maschine erkennen
Pfeil 3.13.1 Shutdown-Hook
Pfeil 3.13.2 Signale
Pfeil 3.14 Zum Weiterlesen
 
Zum Seitenanfang

3.8Teile und herrsche mit Fork und Join * Zur vorigen ÜberschriftZur nächsten Überschrift

 
Zum Seitenanfang

3.8.1Algorithmendesign per »teile und herrsche« Zur vorigen ÜberschriftZur nächsten Ü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:

der Morgen

aufstehen

Augen auf

räkeln

aus dem Bett steigen/fallen

essen

Kühlschrank aufmachen

Essen entnehmen

Essen zubereiten

Essen aufnehmen

Tabelle 3.7Sequenzielle und parallelisierbare Aufgaben beim Aufstehen

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 in 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 zu neuen größeren und 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 so genanntes 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,[ 38 ](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 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.

 
Zum Seitenanfang

3.8.2Nebenläufiges Lösen von D&C-Algorithmen Zur vorigen ÜberschriftZur nächsten Ü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 mögliche 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 nebenläufige 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 log2(n). Die Anzahl der Threads ist 2 + 4 + 8 + 16 + … + 2log2(n) = n, also abhängig von der Problemgröße und in der Größenordnung O(n). Wir könnten nun argumentieren, das Problem mit dem 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ösungen 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 dass 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 nebenläufigen D&C-Ansätzen so einfach nicht.

 
Zum Seitenanfang

3.8.3Fork und Join Zur vorigen ÜberschriftZur nächsten Überschrift

Java integriert in java.util.concurrent ein Framework zum Lösen von nebenläufigen D&C-Algorithmen. 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 zwei Tasks parallel abgearbeitet werden. Gibt es vier Prozessoren, können vielleicht vier Tasks parallel laufen. Tasks lassen sich also grundsätzlich auf eine 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 so genannte Fork/Join-Framework wurde im Rahmen von jsr166y (http://gee.cs.oswego.edu/dl/concurrency-interest/) unter maßgeblicher Mitarbeit von Doug Lea entwickelt. Wie der Name schon andeutet, geht es bei Fork um das Erstellen eines neuen Tasks und bei Join um das Zusammenführen der Ergebnisse. Dabei ist es auf berechnungsintensive parallelisierbare Aufgaben ausgelegt.

Die Fork/Join-Bibliothek stellt dazu die Klasse ForkJoinPool als Koordinator zur Verfügung, einen besonderen ExecutorService bzw. Executor. Für die Task-Beschreibung gibt es die abstrakte Basisklasse ForkJoinTask und davon bisher drei Unterklassen: RecursiveAction (Tasks ohne Ergebnisse), RecursiveTask (Tasks mit Ergebnissen) und seit Java 8 CountedCompleter (siehe umfangreiche Javadoc). Die zentralen Methoden sind fork() und join(), die Klasse ist mit etwa 40 Methoden aber recht umfangreich und unter Java 8 noch etwas gewachsen.

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.[ 39 ](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 nebenläufig das Maximum eines Arrays sucht. Der Start ist:

Listing 3.39com/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(int[]), die auf den ForkJoinPool zurückgreift, um mit invoke(…) den Haupt-Task abzusetzen:

Listing 3.40com/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 Arbeitspaket. Das Objekt referenziert jeweils das Array sowie die Anfangs-/Endposition, ab der sie nach dem Maximum suchen soll:

Listing 3.41com/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 3.42com/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.

 


Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java SE 8 Standard-Bibliothek Java SE 8 Standard-Bibliothek
Jetzt Buch bestellen

 Buchempfehlungen
Zum Rheinwerk-Shop: Java ist auch eine Insel
Java ist auch eine Insel


Zum Rheinwerk-Shop: Professionell entwickeln mit Java EE 8
Professionell entwickeln mit Java EE 8


Zum Rheinwerk-Shop: Besser coden
Besser coden


Zum Rheinwerk-Shop: Entwurfsmuster
Entwurfsmuster


Zum Rheinwerk-Shop: IT-Projektmanagement
IT-Projektmanagement


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2018. Original - https://www.rheinwerk-verlag.de/openbook/
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.

 

 
 


29.09.2022 - Sitemap.xml