Angelika Langer, Klaus Kreft
Java-Core-Programmierung
ISBN: 978-3-86802-602-3

© 2012 entwickler.press
Ein Imprint der Software & Support Media GmbH

Bibliografische Information Der Deutschen Bibliothek

Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.

Ihr Kontakt zum Verlag und Lektorat:
Software & Support Media GmbH
entwickler.press
Darmstädter Landstr. 108
60598 Frankfurt am Main
Tel.: +49 (0)69 630089-0
Fax: +49 (0)69 930089-89
lektorat@entwickler-press.de
http://www.entwickler-press.de

Lektorat: Sebastian Burkart
Korrektorat: Katharina Klassen und Lisa Pychlau
Satz: Dominique Kalbassi

Alle Rechte, auch für Übersetzungen, sind vorbehalten. Reproduktion jeglicher Art (Fotokopie, Nachdruck, Mikrofilm, Erfassung auf elektronischen Datenträgern oder anderen Verfahren) nur mit schriftlicher Genehmigung des Verlags. Jegliche Haftung für die Richtigkeit des gesamten Werks kann, trotz sorgfältiger Prüfung durch Autor und Verlag, nicht übernommen werden. Die im Buch genannten Produkte, Warenzeichen und Firmennamen sind in der Regel durch deren Inhaber geschützt.

Vorwort

Wir haben im Jahr 2002 mit der Veröffentlichung einer Artikelreihe begonnen, die bis heute Themen und Aspekte aufgreift, die den Kern der Programmiersprache Java und ihr Laufzeitsystem betreffen. Es ging uns dabei um Inhalte, die für jeden Java-Entwickler relevant sind, ganz gleich welche Art von Java-Applikation (Client, Server, Desktop, Enterprise etc.) er entwickelt. Im Jahr 2011 ist der Verleger des vorliegenden Buches an uns herangetreten mit der Bitte, die Beiträge der letzten Jahre (2008 bis 2011) als Buch veröffentlichen zu dürfen. In dieser Zeit haben wir uns mit dem Java-Memory-Modell und mit den Garbage-Collection-Mechanismen der Sun JVM befasst. Da es zu diesen Themen keine Veröffentlichungen gibt, die ähnlich umfassend und detailliert sind, haben wir dem Buchprojekt zugestimmt.

Bei der Überarbeitung der Artikel für die Veröffentlichung als Buch haben wir uns entschlossen, den Artikelcharakter der Beiträge im Wesentlichen bei zu behalten. Die einzelnen Kapitel des Buchs sind deshalb so angelegt, dass sie zwar aufeinander Bezug nehmen, aber nicht notwendig der Reihe nach gelesen werden müssen. Es ist durchaus möglich und auch beabsichtigt, dass der Leser selektiv einzelne Kapitel lesen kann. Nachteil dieser Vorgehensweise ist ein gewisses Maß an Redundanz, das wir aber im Hinblick auf die beabsichtigte Verwendung der Texte für angemessen halten.

Generell haben wir uns bemüht, Anglizismen zu vermeiden. Gelegentlich haben wir aber von Lesern der Artikelserie Einwände gegen die Verwendung von englischen Begriffen bekommen. Dabei handelt es sich stets um Fachbegriffe, deren Übersetzung ins Deutsche zwar prinzipiell möglich, aber in der Praxis wohl eher irreführend als hilfreich ist. Wie sollte man beispielsweise den Begriff „Remembered Set“ übersetzen, als „erinnerte Menge“? Uns ist kein deutschsprachiger Text bekannt, der für diesen Fachbegriff aus der Garbage Collection eine Übersetzung verwendet hätte. Deshalb haben wir es auch nicht getan.

Wir wünschen viel Spaß beim Lesen und Stöbern in den Details des Java-Memory-Modells und der Garbage Collection. Für Kommentare, Fragen und Anregungen sind wir unter http://www.AngelikaLanger.com/Forms/Contact.html zu erreichen.

München, den 5. Juli 2011

Angelika Langer und Klaus Kreft

Teil A – Java-Memory-Modell

Im ersten Teil des Buchs erläutern wir Aspekte des Java-Memory-Modells (JMM). Generell wird das Wissen über das Memory-Modell in Java für die Programmierung mit mehreren Threads gebraucht. Detaillierte Kenntnisse des Memory-Modells werden insbesondere durch die zunehmende Verwendung von Multicore-Prozessoren immer wichtiger. Das Wissen über das Memory-Modell wird daher zunehmend als Teil der Allgemeinbildung eines jeden Java-Entwicklers angesehen. Deshalb wollen wir in diesem ersten Teil des Buchs einige der wesentlichen Aspekte des Java-Memory-Modells erläutern.

Um einen ersten Einblick in das Memory-Modell zu geben, betrachten wir in Kapitel 1 die Verwendung des volatile-Schüsselworts. Es wird gelegentlich zur Optimierung benutzt, um die „teure“ Synchronisation mit Locks durch eine „preiswertere“ Alternative mit volatile-Variablen zu ersetzen. Dabei kann der Entwickler leicht Fehler machen, wenn ihm das Java-Memory-Modell nicht geläufig ist. In diesem Kapitel sollen folgende Fragen beantwortet werden: Was bedeutet volatile? Wofür braucht man es? Worauf muss man achten?

In Kapitel 2 erläutern wir die Prinzipien des Java-Memory-Modells. Es definiert Konsistenzregeln für den konkurrierenden Zugriff auf veränderliche Daten und Regeln für die Atomarität von Zugriffen auf gemeinsam verwendete Daten, deren Reihenfolge und die Sichtbarkeit etwaiger Modifikationen an den Daten. Dahinter steht ein mentales Modell, bei dem es einen Hauptspeicherbereich gibt, der für alle Threads sichtbar ist. Zusätzlich hat jeder Thread einen eigenen Cache, den nur er selbst sehen kann. Die Regeln des Java-Memory-Modells kann man sich anhand dieses Bildes als Garantien für Flushes und Refreshes der Thread-lokalen Caches vorstellen.

In Kapitel 3 gehen wir auf die Kosten der Synchronisation und ihre Schwächen in Bezug auf Performance und Skalierbarkeit ein. Wir erläutern, warum man zum Zweck der Optimierung von Performance und Skalierbarkeit in zunehmendem Maße die Synchronisation durch andere Techniken ersetzen kann. Diese alternativen Techniken basieren unter anderem auf der Verwendung von volatile-, final- und atomaren Variablen. Deshalb werden in den folgenden Kapiteln die Memory-Modell-Regeln für volatile-, final- und für atomare Variablen besprochen.

In Kapitel 4 befassen wir uns im Detail mit volatile-Variablen. Es geht um die Speichereffekte, die durch Zugriffe auf volatile-Variablen ausgelöst werden, die Sichtbarkeitsgarantien, die das Java-Memory-Modell für volatile-Variablen gibt, und die Kosten von volatile im Vergleich zur Synchronisation. Von besonderem Interesse sind dabei volatile-Referenzvariablen, weil man die Sichtbarkeitsgarantien meist nicht nur für die Referenz selbst, sondern auch für das referenzierte Objekt haben möchte.

Kapitel 5 betrachtet ein Beispiel aus der Praxis, das zeigt, wie die Verwendung von volatile die Skalierbarkeit verbessert. Es geht um das häufig diskutierte Double-Check-Idiom und seine Varianten.

In Kapitel 6 gehen wir der Frage nach, wann es sinnvoll ist, volatile als Alternative zu Locks zu verwenden. Prinzipiell sind volatile-Variablen preiswerter als die Synchronisation mit Locks. Aber nicht in allen Situationen kann man Synchronisation durch volatile-Variablen ersetzen, denn volatile-Variablen haben eine Reihe von Limitationen. Wir gehen deshalb der Frage nach, wann besser Synchronisation verwendet werden sollte.

In Kapitel 7 erläutern wir die sogenannte Initialisation-Safety-Garantie für final-Felder. Es geht dabei um Garantien für die Sichtbarkeit im Zusammenhang mit der Initialisierung und dem Lesezugriff auf final-Felder. Diese Garantien werden bei der Implementierung von unveränderlichen Typen benötigt.

Manchmal unterstellen Java-Programmierer, dass beim Zugriff auf Objekte von einem unveränderlichen Typ grundsätzlich keine Synchronisation und auch kein volatile erforderlich sei. Das ist leider ein Irrtum. In Kapitel 8 diskutieren wir einige typische Missverständnisse und Fehler.

Die Verwendung von volatile als Ersatz für Synchronisation hat gewisse Grenzen. So ist es beispielsweise nicht möglich, eine atomare Read-Modify-Write-Sequenz auf einer volatile-Variablen auszuführen; es sind nur die einzelnen Lese- und Schreibzugriffe atomar, nicht aber die ganze Sequenz. Als logische Verallgemeinerung von volatile-Variablen gibt es seit Java 5.0 atomare Variablen, die ununterbrechbare Read-Modify-Write-Sequenzen unterstützen. In Kapitel 9 und 10 sehen wir uns die atomaren Variablen und ihre so genannten CAS-(Compare-and-Swap-)Operationen an.

Als Abschluss der Ausführungen zum Java-Memory-Modell nehmen wir in Kapitel 11 die CopyOnWriteArrayList aus dem JDK und zeigen an ihrem Beispiel das Zusammenspiel der Speichereffekte von Synchronisation, volatile und atomaren Referenzen. Letztlich wird verdeutlicht, welche Zusammenhänge die unterschiedlichen und zuvor ausführlich besprochenen Aspekte in der Praxis haben.

1 Einführung in das Java-Memory-Modell

Um einen ersten Einblick in das Memory-Modell zu geben, betrachten wir die Verwendung des volatile-Schüsselworts. Es wird gelegentlich als Optimierung benutzt, um die teure Synchronisation mit Locks durch eine preiswertere Alternative mit volatile-Variablen zu ersetzen. Dabei kann man als Entwickler aber leicht Fehler machen, wenn einem das Java-Memory-Modell nicht geläufig ist. Daher wird im Folgenden darauf eingegangen, was volatile bedeutet, wofür man es braucht und worauf man achten muss.

Unter Java-Programmierern ist allgemein bekannt, dass man bei der Programmierung mit mehreren Threads besonders aufpassen muss, wenn diese parallel ablaufenden Threads auf gemeinsam verwendete, veränderliche Daten (Shared Mutable Data) zugreifen. Dabei gibt es eine Reihe von Aspekten, die man als Programmierer im Auge behalten muss. Das bekannteste Problem ist die Race Condition: Wenn der Zugriff auf die gemeinsam verwendeten Daten in mehreren Schritten erfolgt, dann ist der Zugriff unterbrechbar. Ein Thread etwa macht die ersten Schritte des Zugriffs, wird mittendrin vom Thread Scheduler verdrängt, ein anderer Thread kommt zum Zug, greift auf die halbveränderten Daten zu und modifiziert sie womöglich, wird seinerseits unterbrochen, der erste Thread kommt wieder dran. Er macht weiter, als sei nichts gewesen. Das Resultat dieser konkurrierenden Zugriffe ist unvorhersehbar.

Die übliche Abhilfe ist Synchronisation. Mithilfe von einem Lock (auch Mutex genannt) wird dafür gesorgt, dass der Zugriff auf die gemeinsam verwendeten veränderlichen Daten ununterbrechbar ist. Das funktioniert folgendermaßen: Alle beteiligten Threads, die auf die Daten zugreifen wollen, benutzen ein bestimmtes Lock-Objekt. Vor jedem Zugriff auf die kritischen Daten wird das Lock angefordert, nach dem Beenden aller Zugriffsschritte wird das Lock wieder freigegeben. Da ein Lock immer nur einen Thread als Besitzer haben kann, muss beim Anfordern gewartet werden, bis der aktuelle Besitzer das Lock wieder freigibt. Auf diese Weise ist gesichert, dass immer nur ein Thread zu einer Zeit auf die gemeinsam verwendeten veränderlichen Daten zugreifen kann, weil das Lock besetzt ist und alle anderen Threads warten müssen, bis der Thread fertig ist mit seinem Zugriff.

Diese Grundlagen sind sicher jedem Java-Entwickler geläufig. Denn die Instrumente für die Synchronisation sind in Java direkt in die Sprache eingebettet worden: zum einen in Form des synchronized-Schlüsselworts und zum anderen durch das automatische Anhängen eines Locks an jedes Objekt, das man zwar nicht sieht, das aber immer vorhanden ist und implizit über das synchronized-Schlüsselwort angesprochen wird. Als Alternative gibt es seit Java 5 auch die flexibleren, expliziten Locks (siehe Interface Lock im Package java.util.concurrent.locks).

Nun ist die Verwendung von Locks aber teuer, und deswegen kommt als Optimierung das volatile-Schlüsselwort ins Spiel. Die Kosten der Synchronisation mithilfe von Locks bestehen zum einen im Aufwand, den das Anfordern und Freigeben von Locks für die virtuelle Maschine und das Betriebsystem bedeuten, und zum anderen in der Tatsache, dass Synchronisation Wartezustände auslöst, die den Durchsatz der Anwendung reduzieren.

Beim Anfordern und Freigeben von Locks hat das Laufzeitsystem nämlich ein Menge zu tun: Es werden Systemressourcen angelegt und weggeräumt, es werden Threads in Warteschlangen gestellt oder aus Wartezuständen aufgeweckt, es passieren Kontextwechsel und es müssen Daten-Caches abgeglichen werden. All das führt dazu, dass Synchronisation Zeit und Aufwand kostet, der die Performance der Anwendung reduziert.

Daneben wirkt sich Synchronisation nachteilig auf den Durchsatz der Anwendung aus. Es kann passieren, dass der synchronisierte Zugriff auf gemeinsam verwendete veränderliche Daten zu einem echten Engpass werden kann. Wenn viele Threads gleichzeitig auf gemeinsam verwendete Daten zugreifen wollen und immer nur einer das Lock bekommt und alle anderen warten müssen, dann entsteht ein Stau, der sich negativ auf den Durchsatz der Anwendung auswirkt. Mit anderen Worten, Synchronisation skaliert nicht beliebig, und je weniger Synchronisation gebraucht wird, desto besser ist es. Also sollte man die Synchronisation reduzieren, wo immer es geht.

1.1 Atomare Zugriffe

Wie oben erläutert, wird die Synchronisation gebraucht, um komplexere Zugriffe auf gemeinsam verwendete veränderliche Daten ununterbrechbar zu machen. Wenn die betreffenden Daten aber elementar sind und der Zugriff darauf schon von Natur aus ununterbrechbar (also atomar) ist, dann braucht man doch keine Synchronisation, richtig? Wenn also zum Beispiel die gemeinsam verwendeten Daten lediglich aus einem boolean bestehen, dann sind Zugriffe wie Lesen oder Schreiben atomar. Das garantiert die Sprachdefinition (siehe /JLS/, Kapitel 17). Dort ist festgelegt, dass lesende und schreibende Zugriffe auf Variablen von primitivem Typ (außer long und double) atomar sind. Deshalb wird in der Praxis vielfach die Synchronisation weggelassen, wenn es um konkurriende Zugriffe auf Variablen von primitivem Typ geht. Hier ist ein typisches Beispiel:

public class Processor {
    private boolean connectionPrepared = false;
    public void prepareConnection() {
        // ... open connection ...
        connectionPrepared = true;
    }
    public void start() throws InterruptedException {
        // ... various initializations ...
        while (!connectionPrepared )
            Thread.sleep(500);
        // ... start actual processing ...
    }
}

Wir haben hier zwei Methoden der Klasse Processor, die beide auf das Feld connectionPrepared zugreifen. Die Idee ist, dass die start-Methode in einem Thread ausgeführt wird, der alle vorbereitenden Arbeiten anstößt und dann abwartet, bis alle Vorbereitungen abgeschlossen sind, ehe er die eigentliche Verarbeitung beginnt. Die beiden Threads kommunizieren miteinander über gemeinsam verwendete veränderliche Daten, nämlich das boolean-Feld connectionPrepared. Der eine Thread setzt connectionPrepared auf true, wenn er fertig ist. Der andere Thread beobachtet, ob connectionPrepared auf true gesetzt wurde, um dann mit der eigentlichen Arbeit zu beginnen. Das Lesen und Verändern des boolean-Feldes ist garantiert atomar, deshalb wird keine Synchronisation verwendet. Leider wurde hier ein wesentlicher Aspekt übersehen.

1.2 Sequential Consistency

Der Autor dieser kleinen Klasse ist augenscheinlich davon ausgegangen, dass der eine Thread irgendwann einmal das boolean-Feld connectionPrepared setzen wird und dass der andere Thread den veränderten Wert dann sehen kann. Eine derartige Garantie gibt es in Java aber nicht. Es ist keineswegs so, dass ein Thread immer sofort sehen kann, was ein anderer Thread im Speicher gemacht hat. Es kann passieren, dass der eine Thread das boolean-Feld connectionPrepared auf true gesetzt hat und der andere Thread diese Änderung nie zu sehen bekommt.

Das Schwierige an der Sache ist, dass der beschriebene Effekt nicht auftreten muss, aber auftreten kann. Das heißt, die Klasse hat einen Fehler, der sich unter Umständen nicht bemerkbar macht. In manchen existierenden Anwendungen schlummern derartige Fehler, die bisher nicht entdeckt wurden. Nun ist es so, dass auf Maschinen mit nur einem Single-Core-Prozessor die Fehler häufig nicht auftreten. In einer Multi-Prozessor- oder Multicore-Umgebung kann der Fehler jedoch plötzlich auftreten. Da heute Dual-Core-Prozessoren Standard sind, kann man in der Tat beobachten, wie Anwendungen, die auf einer Single-Core-Maschine tadellos funktioniert haben, unerwartet seltsame Fehler aufweisen, sobald sie auf einer Maschine mit einem Multi-Core-Prozessor ablaufen.

Welchen Fehler hat der Autor der Klasse gemacht? Er hat Annahmen über das Verhalten der virtuellen Maschine gemacht, die unzutreffend sind. Was er unterstellt hat, wird allgemein als Sequential Consistency bezeichnet. Sequential Consistency bedeutet, dass ein Thread, der später drankommt, sehen kann, was die Threads vor ihm im Speicher an den gemeinsam verwendeten Daten gemacht haben. Das ist ein wunderbar simples mentales Modell, das aber leider von Java nicht unterstützt wird. Wir haben a priori keine Sequential Consistency in Java.

Es gibt in der Sprachspezifikation stattdessen eine Reihe von Garantien für die Reihenfolge von Operationen und auch für die Sichtbarkeit von Memory-Modifikationen. Sie sind jedoch wesentlich schwächer als die unserer Intuition entsprechende Sequential Consistency. Wir wollen jetzt nicht das gesamte Memory-Modell von Java aufrollen, sondern nur eine einzige der Regeln aus der Sprachspezifikation herausgreifen, nämlich die Garantien für volatile-Variablen.

1.3 Sichtbarkeitsregeln für volatile-Variablen

Für eine volatile-Variable ist garantiert, dass ein Thread, der die Variable liest, den Wert bekommt, den zuvor ein anderer Thread derselben Variablen zugewiesen hat. Es ist außerdem garantiert, dass der Wert, den ein Thread in einer volatile-Variablen ablegt, allen anderen Threads zugänglich gemacht wird. Das heißt für volatile-Variablen haben wir die gewünschte Sequential Consistency. Allerdings gilt das nur für die Variable selbst: Bei einer Variablen von einem Referenztyp gilt es nur für die Adresse, nicht für das referenzierte Objekt. Die Garantien sind streng genommen sogar noch umfangreicher: Beim Schreiben auf eine volatile-Variable wird nicht nur der neue Inhalt eben jener volatile-Variablen sichtbar gemacht, sondern alle Modifikation, die der Thread zuvor im Speicher gemacht hat. Das interessiert uns aber im Moment nicht. Da das Thema etwas umfangreicher ist, werden wir die Details an einer anderen Stelle näher erläutern. Kehren wir lieber zu unserem fehlerhaften Beispielcode zurück.

Man könnte den Fehler ganz einfach dadurch beheben, dass man das boolean-Feld overHeated als volatile deklariert:

public class Processor {
    private volatile boolean connectionPrepared = false;
    public void prepareConnection() {
        // ... open connection ...
        connectionPrepared = true;
    }
    public void start() throws InterruptedException {
        // ... various initializations ...
        while (!connectionPrepared )
            Thread.sleep(500);
        // ... start actual processing ...
    }
}

Jetzt ist die Klasse korrekt, weil nun garantiert ist, dass die Modifikation, die der eine Thread mithilfe der prepareConnection-Methode am volatile boolean connectionPrepared vornimmt, dem anderen Thread sichtbar gemacht wird. Mit dieser volatile-Deklaration kann es nicht mehr passieren, dass der eine Thread das boolean-Feld ändert und der andere Thread es gar nicht mitbekommt.

1.4 Sichtbarkeitsregeln für Synchronisation

Wir wollen nicht verschweigen, dass man das Problem auch anders lösen kann, nämlich indem man den Zugriff auf das connectionPrepared-Feld synchronisiert:

public class Processor {
    private boolean connectionPrepared = false;
    public synchronized void prepareConnection() {
        // ... open connection ...
        connectionPrepared = true;
    }
    public synchronized void start()
                   throws InterruptedException {
        // ... various initializations ...
        while (!connectionPrepared )
            Thread.sleep(500);
        // ... start actual processing ...
    }
}

Synchronisation hat nicht nur Auswirkungen auf die Ununterbrechbarkeit einer Sequenz von Operationen, sondern hat zusätzlich Auswirkungen auf die Sichtbarkeit von Speichermodifikationen. Wenn ein Lock freigegeben wird, dann ist garantiert, dass alle Modifikationen, die der betreffende Thread im Speicher gemacht hat, allen anderen Threads sichtbar gemacht werden. Umgekehrt ist garantiert, dass ein Thread beim Erhalt eines Locks alle Modifkationen im Speicher zu sehen bekommt, die ein anderer Thread bewirkt hat, der dasselbe Lock vorher gehalten hat. Das heißt im Fall von Synchronisation gilt wieder die intuitive Sequential Consistency. Wer immer und überall korrekt synchronisiert, braucht kein volatile und wird nie Probleme damit haben, dass ein Thread nicht sehen kann, was ein anderer gemacht hat.

Die fehlende Sequential Consistency macht sich erst dann bemerkbar, wenn es nicht synchronisierte Zugriffe auf Variablen gibt, die nicht volatile sind.

Unsere ursprüngliche Überlegung war aber, dass Synchronisation teuer ist und wenn möglich vermieden werden sollte. Da hier die Zugriffe auf das boolean-Feld atomar sind, wird die Synchronisation nicht gebraucht, um für Ununterbrechbarkeit des Datenzugriffs zu sorgen. Deshalb hatten wir die Synchronisation absichtlich weggelassen. Wenn man eine solche Optimierung vornimmt, dann muss man aber das boolean-Feld als volatile deklarieren, um für die Sichtbarkeit der Modifikationen in diesem Feld zu sorgen.

1.5 Zusammenfassung

Wir haben in diesem Kapitel gesehen, dass man Synchronisation eliminiert, um die Performance- und Scalability-Kosten der Synchronisation zu vermeiden. Das kommt immer dann infrage, wenn der Zugriff auf eine gemeinsam verwendete veränderliche Variable sowieso schon atomar ist und deshalb für die Ununterberechbarkeit des Zugriffs keine Synchronisation gebraucht wird. Sobald man aber unsynchronisiert auf eine gemeinsam verwendete veränderliche Variable zugreift, ist nicht mehr gesichert, dass ein Thread sehen kann, was ein anderer Thread zuvor in der Variable abgelegt hat. Solche Variablen, auf die unsynchronisiert zugegriffen wird, müssen als volatile deklariert werden, damit Modifikationen, die ein Thread an der Variablen vornimmt, garantiert allen anderen Threads sichtbar gemacht werden.

In den nächsten Kapiteln wollen wir das Thema vertiefen und uns ansehen, was eigentlich hinter diesen Sichtbarkeitsregeln steckt und wie das mit volatile-Referenzvariablen funktioniert, welche anderen Garantien das Memory-Modell sonst noch zu bieten hat, wie mit final-Variablen umzugehen ist, was atomic-Variablen sind und was sie wiederum mit volatile zu tun haben.

2 Das Java-Memory-Modell im Überblick

Im vorangegangenen Kapitel haben wir erläutert, dass man das volatile-Schlüsselwort zur Optimierung verwendet, um die relative teure Synchronisation von konkurrierenden Zugriffen auf gemeinsam verwendete veränderliche Daten zu vermeiden. Dabei haben wir die Sichtbarkeitsregeln erwähnt, die sich im Zusammenhang mit volatile und Synchronisation aus dem Java-Memory-Modell ergeben. In diesem Kapitel wollen wir einen Überblick über Konsistenzregeln für den konkurrierenden Zugriff auf veränderliche Daten geben. Es geht dabei insbesondere um die Atomarität von Zugriffen auf gemeinsam verwendete Daten, deren Reihenfolge und die Sichtbarkeit etwaiger Modifikationen an den Daten.

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!

Lesen Sie weiter in der vollständigen Ausgabe!