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.3Der Ausführer (Executor) kommt Zur vorigen ÜberschriftZur nächsten Überschrift

Zur nebenläufigen Ausführung eines Runnable ist immer ein Thread notwendig. Obwohl die nebenläufige Abarbeitung von Programmcode ohne Threads nicht möglich ist, sind doch beide sehr stark verbunden, und es wäre gut, wenn das Runnable von dem tatsächlich abarbeitenden Thread etwas getrennt wäre. Das hat mehrere Gründe:

  • Schon beim Erzeugen eines Thread-Objekts muss das Runnable-Objekt im Thread-Konstruktor übergeben werden. Es ist nicht möglich, das Thread-Objekt aufzubauen, dann über eine JavaBean-Setter-Methode das Runnable-Objekt zuzuweisen und anschließend den Thread mit start() zu starten.

  • Wird start() auf dem Thread-Objekt zweimal aufgerufen, so führt der zweite Aufruf zu einer Ausnahme. Ein erzeugter Thread kann also ein Runnable durch zweimaliges Aufrufen von start() nicht gleich zweimal abarbeiten. Für eine erneute Abarbeitung eines Runnable ist also mit unseren bisherigen Mitteln immer ein neues Thread-Objekt nötig.

  • Der Thread beginnt mit der Abarbeitung des Programmcodes vom Runnable sofort nach dem Aufruf von start(). Die Implementierung vom Runnable selbst müsste geändert werden, wenn der Programmcode nicht sofort, sondern später (nächste Tagesschau) oder wiederholt (immer Weihnachten) ausgeführt werden soll.

Wünschenswert ist eine Abstraktion, die das Ausführen des Runnable-Programmcodes von der technischen Realisierung (etwa den Threads) trennt.

 
Zum Seitenanfang

3.3.1Die Schnittstelle Executor Zur vorigen ÜberschriftZur nächsten Überschrift

Anstatt das Runnable direkt an einen Thread und somit an seinen Ausführer zu binden, gibt es eine Abstraktion für alle »Abarbeiter«. Die Schnittstelle Executor schreibt eine Methode vor:

interface java.util.concurrent.Executor
  • void execute(Runnable command)
    Wird später von Klassen implementiert, die ein Runnable abarbeiten können.

Jeder, der nun Befehle über Runnable abarbeitet, ist Executor.

Konkrete Executoren

Von dieser Schnittstelle gibt es bisher zwei wichtige Implementierungen:

  • ThreadPoolExecutor: Die Klasse baut eine Sammlung von Threads auf, den Thread-Pool. Ausführungsanfragen werden von den freien Threads übernommen.

  • ScheduledThreadPoolExecutor. Eine Erweiterung von ThreadPoolExecutor um die Fähigkeit, zu bestimmten Zeiten oder mit bestimmten Wiederholungen Befehle abzuarbeiten.

Die beiden Klassen haben nicht ganz so triviale Konstruktoren, und eine Utility-Klasse vereinfacht den Aufbau dieser speziellen Executor-Objekte.

class java.util.concurrent.Executors
  • static ExecutorService newCachedThreadPool()
    Liefert einen Thread-Pool mit wachsender Größe.

  • static ExecutorService newFixedThreadPool(int nThreads)
    Liefert einen Thread-Pool mit maximal nThreads.

  • static ScheduledExecutorService newSingleThreadScheduledExecutor()

  • static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    Gibt spezielle Executor-Objekte zurück, um Wiederholungen festzulegen.

  • Es gibt über 20 Methoden in Executors, diese Aufzählung hier zeigt nur die, die für uns in den nächsten Abschnitten relevant sind.

Die Schnittstelle ExecutorService, die Executor erweitert

Abbildung 3.5Die Schnittstelle ExecutorService, die Executor erweitert

ExecutorService ist eine Schnittstelle, die Executor erweitert. Unter anderem sind hier Operationen zu finden, die die Ausführer herunterfahren. Im Falle von Thread-Pools ist das nützlich, da die Threads ja sonst nicht beendet würden, weil sie auf neue Aufgaben warten.

 
Zum Seitenanfang

3.3.2Glücklich in der Gruppe – die Thread-Pools Zur vorigen ÜberschriftZur nächsten Überschrift

Eine wichtige statische Methode der Klasse Executors ist newCachedThreadPool(…). Das Ergebnis ist ein ExecutorService-Objekt, eine Implementierung von Executor mit der Methode execute(Runnable):

Listing 3.11com/tutego/insel/thread/concurrent/ThreadPoolDemo.java, main(), Teil 1

Runnable r1 = new Runnable() {
@Override public void run() {
System.out.println( "A1 " + Thread.currentThread() );
System.out.println( "A2 " + Thread.currentThread() );
}
};

Runnable r2 = new Runnable() {
@Override public void run() {
System.out.println( "B1 " + Thread.currentThread() );
System.out.println( "B2 " + Thread.currentThread() );
}
};

Jetzt lässt sich der Thread-Pool als ExecutorService beziehen und lassen sich die beiden Befehlsobjekte als Runnable über execute(…) ausführen:

Listing 3.12com/tutego/insel/thread/concurrent/ThreadPoolDemo.java, main(), Teil 2

ExecutorService executor = Executors.newCachedThreadPool();

executor.execute( r1 );
executor.execute( r2 );

Thread.sleep( 500 );

executor.execute( r1 );
executor.execute( r2 );

executor.shutdown();

Die Ausgabe zeigt sehr schön die Wiederverwendung der Threads:

A1 Thread[pool-1-thread-1,5,main]
A2 Thread[pool-1-thread-1,5,main]
B1 Thread[pool-1-thread-2,5,main]
B2 Thread[pool-1-thread-2,5,main]
B1 Thread[pool-1-thread-1,5,main]
B2 Thread[pool-1-thread-1,5,main]
A1 Thread[pool-1-thread-2,5,main]
A2 Thread[pool-1-thread-2,5,main]

Die toString()-Methode von Thread ist so implementiert, dass zunächst der Name der Threads auftaucht, den die Pool-Implementierung gesetzt hat, dann die Priorität und der Name des Threads, der den neuen Thread gestartet hat. Am neuen Namen ist abzulesen, dass hier zwei Threads von einem Thread-Pool 1 verwendet werden: thread-1 und thread-2. Nach dem Ausführen der beiden Aufträge und der kleinen Warterei sind die Threads fertig und zu neuen Jobs bereit, sodass A1 und A2 beim zweiten Mal mit den wieder freien Threads abgearbeitet werden.

Interessant sind die folgenden drei Operationen zur Steuerung des Pool-Endes:

interface java.util.concurrent.ExecutorService
extends Executor
  • void shutdown()
    Fährt den Thread-Pool herunter. Laufende Threads werden nicht abgebrochen, aber neue Anfragen werden nicht angenommen.

  • boolean isShutdown()
    Wurde der Executor schon heruntergefahren?

  • List<Runnable> shutdownNow()
    Gerade ausführende Befehle werden zum Stoppen angeregt. Die Rückgabe ist eine Liste der zu beendenden Kommandos.

 
Zum Seitenanfang

3.3.3Threads mit Rückgabe über Callable Zur vorigen ÜberschriftZur nächsten Überschrift

Der nebenläufige Thread kann nur über Umwege Ergebnisse zurückgeben. In einer eigenen Klasse, die Runnable erweitert, lässt sich im Konstruktor zum Beispiel eine Datenstruktur übergeben, in die der Thread ein berechnetes Ergebnis hineinlegt. Die Datenstruktur kann dann vom Aufrufer auf Änderungen hin untersucht werden.

Die Java-Bibliothek bietet noch einen anderen Weg, denn während run() in Runnable als Rückgabe void hat, übermittelt call() einer anderen Schnittstelle Callable eine Rückgabe. Zum Vergleich:

interface java.lang.Runnable

  • void run()
    Diese Methode enthält den nebenläufig auszuführenden Programmcode.

interface java.util.concurrent.Callable<V>

  • V call()
    Diese Methode enthält den nebenläufig auszuführenden Programmcode und liefert eine Rückgabe vom Typ V.

Tabelle 3.2Methoden in Runnable und Callable

Die einfache Schnittstelle Callable mit einer Operation

Abbildung 3.6Die einfache Schnittstelle Callable mit einer Operation

Beispiel: Felder sortieren über Callable

Wir wollen ein Beispiel implementieren, das ein Feld sortiert. Das Sortieren soll ein Callable im Hintergrund übernehmen. Ist die Operation beendet, soll der Verweis auf das sortierte Feld zurückgegeben werden. Das Sortieren erledigt wie üblich Arrays.sort(…):

Listing 3.13com/tutego/insel/thread/concurrent/SorterCallable.java, SorterCallable

class SorterCallable implements Callable<byte[]> {

private final byte[] b;

SorterCallable( byte[] b ) {
this.b = b;
}

@Override public byte[] call() {
Arrays.sort( b );
return b;
}
}

Natürlich bringt es wenig, das Callable-Objekt aufzubauen und selbst call() aufzurufen, denn ein Thread soll die Aufgabe im Hintergrund erledigen. Dazu ist jedoch nicht die Klasse Thread selbst zu verwenden, sondern ein ExecutorService, den wir etwa über Executors.newCachedThreadPool() bekommen:

Listing 3.14com/tutego/insel/thread/concurrent/CallableGetDemo.java, main() Ausschnitt

byte[] b = new byte[ 4000000 ];
new Random().nextBytes( b );
Callable<byte[]> c = new SorterCallable( b );
ExecutorService executor = Executors.newCachedThreadPool();
Future<byte[]> result = executor.submit( c );

Der ExecutorService bietet eine submit(Callable)-Methode, die unser Callable annimmt und einen Thread für die Abarbeitung aussucht.

Erinnerungen an die Zukunft – die Future-Rückgabe

Weil das Ergebnis asynchron ankommt, liefert submit(…) ein Future-Objekt zurück, über das wir herausfinden können, ob das Ergebnis schon da ist oder ob wir noch warten müssen. Die Operationen im Einzelnen:

interface java.util.concurrent.Future<V>
  • V get() throws InterruptedException, ExecutionException
    Wartet auf das Ergebnis und gibt es dann zurück. Die Methode blockiert so lange, bis das Ergebnis da ist.

  • V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException
    Wartet eine gegebene Zeit auf das Ergebnis und gibt es dann zurück. Kommt es in der vorgegebenen Dauer nicht, gibt es eine TimeoutException.

  • boolean isDone()
    Wurde die Arbeit beendet oder sogar abgebrochen?

  • boolean cancel(boolean mayInterruptIfRunning)
    Bricht die Arbeit ab.

  • boolean isCancelled()
    Wurde die Arbeit vor dem Ende abgebrochen?

Das Ergebnis von submit(Callable) ist also das Future-Objekt. Eigentlich ist nach einem submit(…) die beste Zeit, noch andere nebenläufige Aufgaben anzustoßen, um dann später mit get(…) das Ergebnis einzusammeln. Das Programmiermuster ist immer gleich: Erst Arbeit an den ExecutorService übergeben, dann etwas anderes machen und später zurückkommen. Da wir in unserem Beispiel jedoch in der Zwischenzeit nichts anderes zu tun haben, als ein Bytefeld zu sortieren, setzen wir das Callable ab und warten mit get() sofort auf das sortierte Feld:

Listing 3.15com/tutego/insel/thread/concurrent/CallableGetDemo.java, main()

byte[] b = new byte[ 4000000 ];
new Random().nextBytes( b );
Callable<byte[]> c = new SorterCallable( b );
ExecutorService executor = Executors.newCachedThreadPool();
Future<byte[]> result = executor.submit( c );
try {
byte[] bs = result.get();
System.out.printf( "%d, %d, %d%n",
bs[0], bs[1], bs[bs.length-1] ); // –128, –128, 127
}
catch ( InterruptedException | ExecutionException e ) {
e.printStackTrace();
}

Da das Feld sortiert ist und der Wertebereich eines Bytes mit –128 bis +127 sehr klein ist, ist vermutlich bei 4.000.000 Werten das kleinste Element der Zufallszahlen –128 und das größte 127.

[zB]Beispiel

Nicht immer ist das potenziell unendliche Blockieren erwünscht. Für diesen Fall ermöglicht die überladene Methode von get(…) eine Parametrisierung mit einer Wartezeit und Zeiteinheit:

Listing 3.16com/tutego/insel/thread/concurrent/CallableGetTimeUnitDemo.java, Ausschnitt

byte[] bs = result.get( 2, TimeUnit.SECONDS );

Ist das Ergebnis nicht innerhalb von 2 Sekunden verfügbar, löst die Methode eine TimeoutException aus, die so aussehen wird:

java.util.concurrent.TimeoutException
at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:228)
at java.util.concurrent.FutureTask.get(FutureTask.java:91)
at com.tutego.insel.thread.concurrent.CallableDemo.main(CallableDemo.java:27)

Ein Runnable mit Zukunft oder als Callable

Aus Gründen der Symmetrie gibt es neben submit(Callable) noch zwei submit(…)-Methoden, die ebenfalls ein Runnable annehmen. Zusammen ergeben sich:

interface java.util.concurrent.ExecutorService
extends Executor
  • <T> Future<T> submit(Callable<T> task)
    Der ExecutorService soll die Aufgabe abarbeiten und Zugriff auf das Ergebnis über die Rückgabe geben.

  • Future<?> submit(Runnable task)
    Der ExecutorService arbeitet das Runnable ab und ermöglicht es, über das Future-Objekt zu erfragen, ob die Ausgabe schon abgearbeitet wurde oder nicht. get() liefert am Ende null.

  • <T> Future<T> submit(Runnable task, T result)
    Wie submit(task), nur: Die get(…)-Anfrage über Future liefert result.

Um ein Runnable in ein Callable umzuwandeln, gibt es noch einige Hilfsmethoden in der Klasse Executors. Dazu zählen die statische Methode callable(Runnable task), die ein Callable<Object> liefert, und die Methode callable(Runnable task, T result), die ein Callable<T> zurückgibt.

 
Zum Seitenanfang

3.3.4Mehrere Callable abarbeiten Zur vorigen ÜberschriftZur nächsten Überschrift

Die Methode submit(Callable)vom ExecutorService nimmt genau ein Callable an und führt es aus:

  • <T> Future<T> submit( Callable<T> task )

Muss eine Anwendung mehrere Callable abarbeiten, kann es natürlich mehrere Aufrufe von submit(Callable) geben. Doch ein ExecutorService kann von sich aus mehrere Callable abarbeiten. Dabei gibt es zwei alternative Varianten:

  • Alle Callable einer Liste werden ausgeführt, und das Ergebnis ist eine Liste von Future-Objekten.

  • Alle Callable einer Liste werden ausgeführt, doch der erste, der mit der Arbeit fertig wird, ergibt das Resultat.

Das ergibt zwei Methoden, und da sie zusätzlich noch mit einer Zeitbeschränkung ausgestattet werden, sind es vier:

interface java.util.concurrent.ExecutorService
extends Executor
  • <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
    throws InterruptedException
    Führt alle Ausgaben aus. Liefert eine Liste von Future-Objekten, die die Ergebnisse repräsentieren.

  • <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
    long timeout, TimeUnit unit) throws InterruptedException
    Führt alle Ausgaben aus und wird die Ergebnisse als Liste von Future-Objekten liefern, solange die Zeit timeout in der gegebenen Zeiteinheit nicht überschritten wird.

  • <T> T invokeAny(Collection<? extends Callable<T>> tasks)
    throws InterruptedException, ExecutionException
    Führt alle Aufgaben aus, aber liefert das Ergebnis eines Ausführers, der als Erster fertig ist. Ein get(…) wird also nie warten müssen.

  • <T> T invokeAny(Collection<? extends Callable<T>> tasks,
    long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException
    Führt alle Aufgaben aus, gilt aber nur für eine beschränkte Zeit. Das erste Ergebnis eines Callable-Objekts, das in der Zeit fertig wird, gibt invokeAny(…) zurück.

 
Zum Seitenanfang

3.3.5ScheduledExecutorService für wiederholende Ausgaben und Zeitsteuerungen nutzen Zur vorigen ÜberschriftZur nächsten Überschrift

Die Klasse ScheduledThreadPoolExecutor ist eine weitere Klasse neben ThreadPoolExecutor, die die Schnittstellen Executor und ExecutorService implementiert. Die wichtige Schnittstelle, die diese Klasse außerdem implementiert, ist aber ScheduledExecutorService – sie schreibt scheduleXXX(…)-Operationen vor, um ein Runnable oder Callable zu bestimmten Zeiten und Wiederholungen auszuführen. (Zwar gibt es mit dem java.util.Timer etwas Ähnliches, doch der ScheduledThreadPoolExecutor nutzt Threads aus dem Pool.)

Das folgende Beispiel führt nach einer Startzeit von 1 Sekunde alle 2 Sekunden eine Ausgabe aus:

Listing 3.17com/tutego/insel/thread/concurrent/ScheduledExecutorServiceDemo.java. main()

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool( 1 );
scheduler.scheduleAtFixedRate(
new Runnable() {
@Override public void run() {
System.out.println( "Tata" );
}
},
1 /* Startverzögerung */,
2 /* Dauer */,
TimeUnit.SECONDS );

Nach 1 Sekunde Startverzögerung bekommen wir jede zweite Sekunde ein »Tata«.

 


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.03.2024 - Sitemap.xml