Design für Gewindesicherheit

Vor sechs Monaten begann ich eine Reihe von Artikeln über das Entwerfen von Klassen und Objekten. In diesem Monat Design - Techniken Spalte, werde ich , dass die Serie durch einen Blick auf Design - Prinzipien , der Sorge Thread - Sicherheit fortsetzen. In diesem Artikel erfahren Sie, was Thread-Sicherheit ist, warum Sie sie benötigen, wann Sie sie benötigen und wie Sie sie erhalten.

Was ist Gewindesicherheit?

Thread-Sicherheit bedeutet einfach, dass die Felder eines Objekts oder einer Klasse immer einen gültigen Status beibehalten, wie er von anderen Objekten und Klassen beobachtet wird, selbst wenn sie gleichzeitig von mehreren Threads verwendet werden.

Eine der ersten Richtlinien, die ich in dieser Spalte vorgeschlagen habe (siehe "Entwerfen der Objektinitialisierung"), besteht darin, dass Sie Klassen so entwerfen sollten, dass Objekte vom Beginn ihrer Lebensdauer bis zum Ende einen gültigen Zustand beibehalten. Wenn Sie diesen Rat befolgen und Objekte erstellen, deren Instanzvariablen alle privat sind und deren Methoden nur ordnungsgemäße Statusübergänge für diese Instanzvariablen vornehmen, sind Sie in einer Umgebung mit nur einem Thread in guter Verfassung. Sie können jedoch in Schwierigkeiten geraten, wenn weitere Themen hinzukommen.

Mehrere Threads können Probleme für Ihr Objekt bedeuten, da der Status Ihres Objekts während der Ausführung einer Methode häufig vorübergehend ungültig sein kann. Wenn nur ein Thread die Methoden des Objekts aufruft, wird immer nur eine Methode ausgeführt, und jede Methode kann beendet werden, bevor eine andere Methode aufgerufen wird. In einer Single-Thread-Umgebung kann jede Methode sicherstellen, dass ein vorübergehend ungültiger Status in einen gültigen Status geändert wird, bevor die Methode zurückgegeben wird.

Sobald Sie jedoch mehrere Threads einführen, unterbricht die JVM möglicherweise den Thread, der eine Methode ausführt, während sich die Instanzvariablen des Objekts noch in einem vorübergehend ungültigen Zustand befinden. Die JVM könnte dann einem anderen Thread die Möglichkeit geben, sie auszuführen, und dieser Thread könnte eine Methode für dasselbe Objekt aufrufen. All Ihre harte Arbeit, um Ihre Instanzvariablen privat zu machen und Ihre Methoden nur gültige Zustandstransformationen durchzuführen, reicht nicht aus, um zu verhindern, dass dieser zweite Thread das Objekt in einem ungültigen Zustand beobachtet.

Ein solches Objekt wäre nicht threadsicher, da in einer Multithread-Umgebung das Objekt beschädigt werden oder einen ungültigen Zustand aufweisen könnte. Ein threadsicheres Objekt behält immer einen gültigen Status bei, wie er von anderen Klassen und Objekten auch in einer Multithread-Umgebung beobachtet wird.

Warum sich um die Gewindesicherheit sorgen?

Es gibt zwei wichtige Gründe, warum Sie beim Entwerfen von Klassen und Objekten in Java über die Thread-Sicherheit nachdenken müssen:

  1. Die Unterstützung für mehrere Threads ist in die Java-Sprache und -API integriert

  2. Alle Threads in einer Java Virtual Machine (JVM) verwenden denselben Heap- und Methodenbereich

Da Multithreading in Java integriert ist, kann es sein, dass jede von Ihnen entworfene Klasse von mehreren Threads gleichzeitig verwendet wird. Sie müssen (und sollten) nicht jede Klasse, die Sie entwerfen, threadsicher machen, da die Thread-Sicherheit nicht kostenlos ist. Sie sollten jedoch bei jedem Entwurf einer Java-Klasse zumindest an die Thread-Sicherheit denken . Eine Beschreibung der Kosten für die Thread-Sicherheit und Richtlinien dazu, wann Klassen threadsicher gemacht werden sollen, finden Sie weiter unten in diesem Artikel.

Angesichts der Architektur der JVM müssen Sie sich nur mit Instanz- und Klassenvariablen befassen, wenn Sie sich um die Thread-Sicherheit sorgen. Da alle Threads denselben Heap verwenden und auf dem Heap alle Instanzvariablen gespeichert sind, können mehrere Threads versuchen, die Instanzvariablen desselben Objekts gleichzeitig zu verwenden. Da alle Threads denselben Methodenbereich verwenden und im Methodenbereich alle Klassenvariablen gespeichert sind, können mehrere Threads gleichzeitig versuchen, dieselben Klassenvariablen gleichzeitig zu verwenden. Wenn Sie sich dafür entscheiden, eine Klasse threadsicher zu machen, besteht Ihr Ziel darin, die Integrität der in dieser Klasse deklarierten Instanz- und Klassenvariablen in einer Multithread-Umgebung zu gewährleisten.

Sie müssen sich keine Gedanken über den Multithread-Zugriff auf lokale Variablen, Methodenparameter und Rückgabewerte machen, da sich diese Variablen auf dem Java-Stack befinden. In der JVM erhält jeder Thread einen eigenen Java-Stack. Kein Thread kann lokale Variablen, Rückgabewerte oder Parameter eines anderen Threads sehen oder verwenden.

In Anbetracht der Struktur der JVM sind lokale Variablen, Methodenparameter und Rückgabewerte von Natur aus "threadsicher". Instanzvariablen und Klassenvariablen sind jedoch nur threadsicher, wenn Sie Ihre Klasse entsprechend gestalten.

RGBColor # 1: Bereit für einen einzelnen Thread

Betrachten Sie als Beispiel für eine Klasse, die nicht threadsicher ist RGBColor, die unten gezeigte Klasse. Instanzen dieser Klasse stellen eine Farbe in drei privaten Instanzvariablen gespeichert: r, g, und b. In Anbetracht der unten gezeigten Klasse RGBColorwürde ein Objekt sein Leben in einem gültigen Zustand beginnen und nur Übergänge im gültigen Zustand vom Beginn seines Lebens bis zum Ende erfahren - jedoch nur in einer Umgebung mit einem Thread.

// In der Datei threads / ex1 / RGBColor.java // Instanzen dieser Klasse sind NICHT threadsicher. öffentliche Klasse RGBColor {private int r; private int g; private int b; öffentliche RGBColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; this.g = g; this.b = b; } public void setColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; this.g = g; this.b = b; } / ** * gibt Farbe in einem Array von drei Ints zurück: R, G und B * / public int [] getColor () {int [] retVal = new int [3]; retVal [0] = r; retVal [1] = g; retVal [2] = b; return retVal; } public void invert () {r = 255 - r; g = 255 - g; b = 255 - b; } private static void checkRGBVals (int r, int g, int b) {if (r 255 || g 255 || b <0 || b> 255) {neue IllegalArgumentException () auslösen; }}}

Da die drei Instanzvariablen, ints r, gund bsind privat, können die einzige Möglichkeit , anderen Klassen und Objekte zugreifen oder die Werte dieser Variablen beeinflussen über ist RGBColor‚s Konstruktor und Methoden. Das Design des Konstruktors und der Methoden garantiert, dass:

  1. RGBColorDer Konstruktor von 'gibt den Variablen immer die richtigen Anfangswerte

  2. Methoden setColor()und invert()führen immer gültige Zustandstransformationen für diese Variablen durch

  3. Die Methode getColor()gibt immer eine gültige Ansicht dieser Variablen zurück

Beachten Sie, dass fehlerhafte Daten, die an den Konstruktor oder die setColor()Methode übergeben werden, abrupt mit einem abgeschlossen werden InvalidArgumentException. Die checkRGBVals()Methode, die diese Ausnahme wirft in der Tat definiert , was es für ein RGBColorObjekt gültig zu sein: die Werte aller drei Variablen r, gund bmuss zwischen 0 und 255, inklusive sein. Um gültig zu sein, muss die durch diese Variablen dargestellte Farbe die neueste Farbe sein, die entweder an den Konstruktor oder die setColor()Methode übergeben oder von der invert()Methode erzeugt wird.

Wenn Sie in einer Single-Thread-Umgebung setColor()Blau aufrufen und übergeben, ist das RGBColorObjekt bei der setColor()Rückgabe blau . Wenn Sie dann getColor()dasselbe Objekt aufrufen , wird es blau. In einer Gesellschaft mit einem Thread RGBColorverhalten sich Instanzen dieser Klasse gut.

Wirf einen gleichzeitigen Schraubenschlüssel in die Werke

Leider kann dieses glückliche Bild eines gut erzogenen RGBColorObjekts unheimlich werden, wenn andere Threads in das Bild eintreten. In einer Multithread-Umgebung sind Instanzen der RGBColoroben definierten Klasse zwei Arten von schlechtem Verhalten ausgesetzt: Schreib- / Schreibkonflikte und Lese- / Schreibkonflikte.

Schreib- / Schreibkonflikte

Stellen Sie sich vor, Sie haben zwei Threads, einen Thread mit dem Namen "rot" und einen mit dem Namen "blau". Beide Threads versuchen, die Farbe desselben RGBColorObjekts festzulegen: Der rote Thread versucht, die Farbe auf Rot festzulegen. Der blaue Faden versucht, die Farbe auf Blau zu setzen.

Beide Threads versuchen, gleichzeitig in die Instanzvariablen desselben Objekts zu schreiben. Wenn der Thread-Scheduler diese beiden Threads genau richtig verschachtelt, stören sich die beiden Threads versehentlich gegenseitig und führen zu einem Schreib- / Schreibkonflikt. Dabei beschädigen die beiden Threads den Status des Objekts.

Das nicht synchronisierteRGBColor Applet

Das folgende Applet mit dem Namen Unsynchronized RGBColor zeigt eine Folge von Ereignissen, die zu einem beschädigten RGBColorObjekt führen können. Der rote Faden versucht unschuldig, die Farbe auf Rot zu setzen, während der blaue Faden unschuldig versucht, die Farbe auf Blau zu setzen. Am Ende RGBColorrepräsentiert das Objekt weder Rot noch Blau, sondern die beunruhigende Farbe Magenta.

Aus irgendeinem Grund lässt Ihr Browser Sie auf diese Weise kein cooles Java-Applet sehen.

RGBColorDrücken Sie die Step-Taste des Applets , um die Abfolge der Ereignisse zu durchlaufen, die zu einem beschädigten Objekt führen . Drücken Sie Zurück, um einen Schritt zu sichern, und Zurücksetzen, um zum Anfang zurückzukehren. Während Sie fortfahren, erklärt eine Textzeile am unteren Rand des Applets, was während jedes Schritts passiert.

For those of you who can't run the applet, here's a table that shows the sequence of events demonstrated by the applet:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes setColor(255, 0, 0) 0 0 0  
red checkRGBVals(255, 0, 0); 0 0 0  
red this.r = 255; 0 0 0  
red this.g = 0; 255 0 0  
red this.b = 0; 255 0 0  
red red thread returns 255 0 0  
blue later, blue thread continues 255 0 0  
blue this.b = 255 255 0 0  
blue blue thread returns 255 0 255  
none object represents magenta 255 0 255  

As you can see from this applet and table, the RGBColor is corrupted because the thread scheduler interrupts the blue thread while the object is still in a temporarily invalid state. When the red thread comes in and paints the object red, the blue thread is only partially finished painting the object blue. When the blue thread returns to finish the job, it inadvertently corrupts the object.

Read/write conflicts

Another kind of misbehavior that may be exhibited in a multithreaded environment by instances of this RGBColor class is read/write conflicts. This kind of conflict arises when an object's state is read and used while in a temporarily invalid state due to the unfinished work of another thread.

For example, note that during the blue thread's execution of the setColor() method above, the object at one point finds itself in the temporarily invalid state of black. Here, black is a temporarily invalid state because:

  1. It is temporary: Eventually, the blue thread intends to set the color to blue.

  2. It is invalid: No one asked for a black RGBColor object. The blue thread is supposed to turn a green object into blue.

If the blue thread is preempted at the moment the object represents black by a thread that invokes getColor() on the same object, that second thread would observe the RGBColor object's value to be black.

Here's a table that shows a sequence of events that could lead to just such a read/write conflict:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes getColor() 0 0 0  
red int[] retVal = new int[3]; 0 0 0  
red retVal[0] = 0; 0 0 0  
red retVal[1] = 0; 0 0 0  
red retVal[2] = 0; 0 0 0  
red return retVal; 0 0 0  
red red thread returns black 0 0 0  
blue later, blue thread continues 0 0 0  
blue this.b = 255 0 0 0  
blue blue thread returns 0 0 255  
none object represents blue 0 0 255  

As you can see from this table, the trouble begins when the blue thread is interrupted when it has only partially finished painting the object blue. At this point the object is in a temporarily invalid state of black, which is exactly what the red thread sees when it invokes getColor() on the object.

Three ways to make an object thread-safe

There are basically three approaches you can take to make an object such as RGBThread thread-safe:

  1. Synchronize critical sections
  2. Make it immutable
  3. Use a thread-safe wrapper

Approach 1: Synchronizing the critical sections

The most straightforward way to correct the unruly behavior exhibited by objects such as RGBColor when placed in a multithreaded context is to synchronize the object's critical sections. An object's critical sections are those methods or blocks of code within methods that must be executed by only one thread at a time. Put another way, a critical section is a method or block of code that must be executed atomically, as a single, indivisible operation. By using Java's synchronized keyword, you can guarantee that only one thread at a time will ever execute the object's critical sections.

To take this approach to making your object thread-safe, you must follow two steps: you must make all relevant fields private, and you must identify and synchronize all the critical sections.

Step 1: Make fields private

Synchronisation bedeutet, dass jeweils nur ein Thread ein Stück Code ausführen kann (ein kritischer Abschnitt). Obwohl es sich um Felder handelt, auf die Sie den Zugriff auf mehrere Threads koordinieren möchten, koordiniert der Java-Mechanismus dazu den Zugriff auf Code. Dies bedeutet, dass Sie nur dann den Zugriff auf diese Daten steuern können, wenn Sie die Daten privat machen, indem Sie den Zugriff auf den Code steuern, der die Daten manipuliert.