JavaBeans: Eigenschaften, Ereignisse und Thread-Sicherheit

Java ist eine dynamische Sprache, die benutzerfreundliche Multithreading-Sprachkonstrukte und Unterstützungsklassen enthält. Viele Java-Programme verwenden Multithreading, um die Parallelität interner Anwendungen auszunutzen, die Netzwerkleistung zu verbessern oder die Reaktion der Benutzer auf Feedback zu beschleunigen. Die meisten Java-Laufzeiten verwenden Multithreading, um die Garbage Collection-Funktion von Java zu implementieren. Schließlich ist das AWT für seine Funktion auch auf separate Threads angewiesen. Kurz gesagt, selbst die einfachsten Java-Programme werden in einer aktiven Multithreading-Umgebung geboren.

Java-Beans werden daher auch in einer solch dynamischen Multithread-Umgebung eingesetzt, und hierin liegt die klassische Gefahr, auf Rennbedingungen zu stoßen . Rennbedingungen sind zeitabhängige Programmflussszenarien, die zu einer Beschädigung des Status (Programmdaten) führen können. Im folgenden Abschnitt werde ich zwei solche Szenarien detailliert beschreiben. Jede Java-Bean muss unter Berücksichtigung der Race-Bedingungen entworfen werden, damit eine Bean der gleichzeitigen Verwendung durch mehrere Client-Threads standhält.

Multithreading-Probleme mit einfachen Eigenschaften

Bean-Implementierungen müssen davon ausgehen, dass mehrere Threads gleichzeitig auf eine einzelne Bean-Instanz zugreifen und / oder diese ändern. Betrachten Sie als Beispiel für eine nicht ordnungsgemäß implementierte Bean (in Bezug auf das Multithreading-Bewusstsein) die folgende BrokenProperties-Bean und das zugehörige MTProperties-Testprogramm:

BrokenProperties.java

import java.awt.Point;

// Demo Bean, die nicht vor der Verwendung mehrerer Threads schützt.

öffentliche Klasse BrokenProperties erweitert Point {

// ------------------------------------------------ ------------------- // set () / get () für 'Spot'-Eigenschaft // --------------- -------------------------------------------------- - -

public void setSpot (Point point) {// 'spot' setter this.x = point.x; this.y = point.y;

} public Point getSpot () {// 'spot' getter return this; }} // Ende der Bean / Klasse BrokenProperties

MTProperties.java

import java.awt.Point; Dienstprogramme importieren. *; import utilities.beans. *;

öffentliche Klasse MTProperties erweitert Thread {

protected BrokenProperties myBean; // die Zielbohne zu schlagen ..

protected int myID; // Jeder Thread trägt ein bisschen ID

// ------------------------------------------------ ------------------- // main () Einstiegspunkt // ---------------------- --------------------------------------------- Public static void Main ( String [] args) {

BrokenProperties bean; Faden Faden;

bean = (BrokenProperties) BeansKit.newBean ("BrokenProperties");

for (int i = 0; i <20; i ++) {// 20 Threads starten, um Bean-Thread zu bash = neue MTProperties (Bean, i); // Threads erhalten Zugriff auf Bean thread.start (); }} // ---------------------------------------------- --------------------- // MTProperties-Konstruktor // ----------------------- ----------------------------------------

öffentliche MTProperties (BrokenProperties-Bean, int id) {this.myBean = bean; // notiere die Bean um diese zu adressieren.myID = id; // beachte wer wir sind} // ------------------------------------- -------------------------- // die Thread-Hauptschleife: // für immer tun // neuen zufälligen Punkt mit x == y // erstellen Sagen Sie Bean, dass sie Point als neue 'Spot'-Eigenschaft übernehmen soll. // Fragen Sie Bean, wie ihre' Spot'-Eigenschaft jetzt eingestellt ist. // Wirf einen Wackelwurf, wenn Spot x nicht gleich Spot y ist. -------------------------------------------------- -------- public void run () {int someInt; Punkt Punkt = neuer Punkt ();

while (true) {someInt = (int) (Math.random () * 100); point.x = someInt; point.y = someInt; myBean.setSpot (Punkt);

point = myBean.getSpot (); if (point.x! = point.y) {System.out.println ("Bean beschädigt! x =" + point.x + ", y =" + point.y); System.exit (10); } System.out.print ((char) ('A' + myID)); System.out.flush (); }}} // Ende der Klasse MTProperties

Hinweis: Das von importierte Dienstprogrammpaket MTPropertiesenthält wiederverwendbare Klassen und statische Methoden, die vom Autor für das Buch entwickelt wurden.

Die beiden obigen Quellcodelisten definieren eine Bean namens BrokenProperties und die Klasse MTProperties, mit der die Bean innerhalb von 20 laufenden Threads ausgeführt wird. Lassen Sie uns folgen MTProperties" main()Einstiegspunkt: Zuerst es instanziiert eine BrokenProperties Bohne, gefolgt von der Erstellung und Starten von 20 Threads. Klasse MTPropertieserweitert java.lang.Thread, so alles , was wir tun müssen , um Klasse zu verwandeln MTPropertiesin ein Gewinde ist außer Kraft zu setzen Klasse Thread‚s - run()Methode. Der Konstruktor für unsere Threads verwendet zwei Argumente: das Bean-Objekt, mit dem der Thread kommuniziert, und eine eindeutige Identifikation, mit der die 20 Threads zur Laufzeit leicht unterschieden werden können.

Das geschäftliche Ende dieser Demo ist unsere run()Methode im Unterricht MTProperties. Hier schleifen wir für immer und erstellen zufällige neue (x, y) Punkte, jedoch mit der folgenden Eigenschaft: Ihre x-Koordinate entspricht immer ihrer y-Koordinate. Diese zufälligen Punkte werden an die setSpot()Setter-Methode der Bean übergeben und dann sofort mit der getSpot()Getter-Methode zurückgelesen. Sie würden erwarten, dass die spotEigenschaft read mit dem zufälligen Punkt identisch ist, der vor einigen Millisekunden erstellt wurde. Hier ist eine Beispielausgabe des Programms, wenn es über die Befehlszeile aufgerufen wird:

ABBBBBBBBBBBBBBBBBBDJJJJJJJJJJJJJJJJJJJJEGHHHHHHHHHHHHHHHHHHSSSSSSSSSSSSSS IICCBBBBBBBBBBBBBBBBBKBDLJMBOPLQNRTPHHHHHHHHFFFFFFFFFFFFSSSSSSSSSSSSSSSSSS FFFFFFFFFFFFFFFAAAAAACCCCCCCKKKKKKKKKKKKKKKKKMMMMMMMMMMMMMMMMMMMMMMMMMMMDD JEOQQQQQQQQQQQQQQQRRRRRRRRRRRRRRRRRBBBBBBBBBBBBBBBTTTTTTTTTTTTTTTTLPPPPPPP PPPPGGHHHFFFFFFFFIIIIIIIIIIIIIISSSSSSSSSSSSSSSSSSSSACCCCCCCCCCCCCCCCCCCKMD QQQQQQNNNNNNNNNNNNNNNNRRRRRTRRHHHHHHHHHFFFFFFFFFFFFFFFFFFIIIIIIIIIIIIIIIII MMMJEEEEEEEEEEDDDDEEEEEEEEEOOOOOOOOOOOOOOOOOOOOOOOOOOOQNNNNNNNNBTLPLRGFFFF FFFFFFFFFIIAAAAAAAAAAAAAAAAASSSSSSSSSSSSSSSSSSKKKKKKKKKKKKKKKKCCCCCCCMMJAA AACBean beschädigt! x = 67, y = 13 OOOOOOOOOOOOOOOOOOOO

Die Ausgabe zeigt die 20 parallel laufenden Threads (soweit ein menschlicher Beobachter dies tut). Jeder Thread verwendet die ID, die er zur Bauzeit erhalten hat, um einen der Buchstaben A bis T , die ersten 20 Buchstaben des Alphabets, zu drucken . Sobald ein Thread feststellt, dass die Rückleseeigenschaft spotnicht der programmierten Eigenschaft von x = y entspricht, druckt der Thread eine Meldung "Bean beschädigt" und hält das Experiment an.

Was Sie sehen, ist der zustandsverfälschende Nebeneffekt einer Race-Bedingung im setSpot()Code der Bean . Hier ist noch einmal diese Methode:

public void setSpot (Point point) {// 'spot' setter this.x = point.x; this.y = point.y; }}

Was könnte jemals in einem so einfachen Code schief gehen? Stellen Sie sich Thread A- Aufruf setSpot()mit einem Punktargument von (67,67) vor. Wenn wir jetzt die Uhr des Universums verlangsamen, damit die Java Virtual Machine (JVM) jede Java-Anweisung einzeln ausführen kann, können wir uns vorstellen, dass Thread A die x-Koordinaten-Kopieranweisung ( this.x = point.x;) ausführt und dann plötzlich Thread A wird vom Betriebssystem eingefroren, und Thread C soll eine Weile ausgeführt werden. In seinem vorherigen Betriebszustand hatte Thread C gerade seinen eigenen neuen Zufallspunkt (13,13) erstellt, sich setSpot()selbst aufgerufen und dann eingefroren, um Platz für Thread M zu schaffen, unmittelbar nachdem die x-Koordinate auf 13 gesetzt wurde. Der wieder aufgenommene Thread C setzt nun seine programmierte Logik fort: y auf 13 setzen und prüfen, ob die Spot-Eigenschaft gleich ist (13, 13), aber auf mysteriöse Weise feststellen, dass dies der Fall ist geändert in einen illegalen Zustand von (67, 13); Die x-Koordinate ist die Hälfte des Zustands, auf den sich Thread A eingestellt hat spot, und die y-Koordinate ist die Hälfte des Zustands, auf den sich Thread C eingestellt hatspot . Das Endergebnis ist, dass die BrokenProperties-Bean einen intern inkonsistenten Status aufweist: eine fehlerhafte Eigenschaft.

Immer wenn eine nichtatomare Datenstruktur (dh eine Struktur, die aus mehr als einem Teil besteht) von mehr als einem Thread gleichzeitig geändert werden kann, müssen Sie die Struktur mit einer Sperre schützen. In Java erfolgt dies mit dem synchronizedSchlüsselwort.

Warnung: Beachten Sie im Gegensatz zu allen anderen Java-Typen, dass Java dies nicht garantiert longund doubleatomar behandelt wird! Dies liegt daran , longund double64 Bits erfordern, die Wortlänge zweimal die Länge der meisten modernen CPU - Architekturen (32 Bit). Sowohl das Laden als auch das Speichern einzelner Maschinenwörter sind an sich atomare Operationen, aber das Verschieben von 64-Bit-Entitäten erfordert zwei solche Verschiebungen, und diese werden aus dem üblichen Grund nicht durch Java geschützt: Leistung. (Bei einigen CPUs kann der Systembus gesperrt werden, um Mehrwortübertragungen atomar durchzuführen. Diese Funktion ist jedoch nicht für alle CPUs verfügbar und in jedem Fall unglaublich teuer für alle longoder doubleManipulationen!) Selbst wenn eine Eigenschaft besteht von nur einem einzigen longoder einem einzigendoublesollten Sie die Vorsichtsmaßnahmen für die vollständige Verriegelung treffen, um Ihre Longs oder Double vor plötzlicher völliger Beschädigung zu schützen.

Das synchronizedSchlüsselwort markiert einen Codeblock als atomaren Schritt. Der Code kann nicht "geteilt" werden, wie wenn ein anderer Thread den Code unterbricht, um diesen Block möglicherweise erneut einzugeben (daher der Begriff " Wiedereintrittscode" ; der gesamte Java-Code sollte wiedereintrittsfähig sein). Die Lösung für unsere BrokenProperties-Bean ist trivial: Ersetzen Sie einfach ihre setSpot()Methode durch folgende:

public void setSpot (Point point) {// 'spot' setter synchronisiert (this) {this.x = point.x; this.y = point.y; }}

Oder alternativ dazu:

public synchronized void setSpot (Point point) {// 'spot' setter this.x = point.x; this.y = point.y; }}

Beide Ersetzungen sind vollkommen gleichwertig, obwohl ich den ersten Stil bevorzuge, da er die genaue Funktion des synchronizedSchlüsselworts deutlicher zeigt: Ein synchronisierter Block ist immer mit einem Objekt verknüpft, das gesperrt wird. Mit gesperrt meine ich, dass die JVM zuerst versucht, eine Sperre (dh exklusiven Zugriff) für das Objekt zu erhalten (dh exklusiven Zugriff darauf zu erhalten) oder wartet, bis das Objekt entsperrt wird, wenn es von einem anderen Thread gesperrt wurde. Der Sperrvorgang garantiert, dass jedes Objekt jeweils nur von einem Thread gesperrt werden kann (oder gehört).

Die synchronized (this)Syntax spiegelt also eindeutig den internen Mechanismus wider: Das Argument in den Klammern ist das Objekt, das gesperrt werden soll (das aktuelle Objekt), bevor der Codeblock eingegeben wird. Die alternative Syntax, bei der das synchronizedSchlüsselwort als Modifikator in einer Methodensignatur verwendet wird, ist einfach eine Kurzversion der ersteren.

Warnung: Wenn statische Methoden markiert sind synchronized, muss kein thisObjekt gesperrt werden. Einem aktuellen Objekt sind nur Instanzmethoden zugeordnet. Wenn Klassenmethoden synchronisiert werden, java.lang.Classwird stattdessen das Objekt verwendet, das der Klasse der Methode entspricht. Dieser Ansatz hat schwerwiegende Auswirkungen auf die Leistung, da eine Sammlung von Klasseninstanzen ein einzelnes zugeordnetes ClassObjekt gemeinsam nutzt. Wenn dieses ClassObjekt gesperrt wird, können alle Objekte dieser Klasse (ob 3, 50 oder 1000!) nicht dieselbe statische Methode aufrufen. Vor diesem Hintergrund sollten Sie zweimal überlegen, bevor Sie die Synchronisation mit statischen Methoden verwenden.

Denken Sie in der Praxis immer an die explizit synchronisierte Form, da Sie so den kleinstmöglichen Codeblock innerhalb einer Methode "atomisieren" können. Die Kurzform "atomisiert" die gesamte Methode, was aus Leistungsgründen oft nicht das ist , was Sie wollen. Sobald ein Thread eines atomaren Blocks von Code eingegeben hat, dass kein anderer Thread ausführen muss jede können dies tun synchronisierten Code auf das gleiche Objekt.

Tip: When a lock is obtained on an object, then all synchronized code for that object's class will become atomic. Therefore, if your class contains more than one data structure that needs to be treated atomically, but those data structures are otherwise independent of each other, then another performance bottleneck can arise. Clients calling synchronized methods that manipulate one internal data structure will block all other clients that call the other methods that deal with any other atomic data structures of your class. Clearly, you should avoid such situations by splitting the class into smaller classes that handle only one data structure to be treated atomically at a time.

The JVM implements its synchronization feature by creating queues of threads waiting for an object to become unlocked. While this strategy is great when it comes to protecting the consistency of composite data structures, it can result in multithreaded traffic jams when a less-than-efficient section of code is marked as synchronized.

Therefore, always pay attention to just how much code you synchronize: it should be the absolute minimum necessary. For example, imagine our setSpot() method originally consisted of:

public void setSpot(Point point) { // 'spot' setter log.println("setSpot() called on " + this.toString() ); this.x = point.x; this.y = point.y; } 

Obwohl die printlnAnweisung möglicherweise logisch zur setSpot()Methode gehört, ist sie nicht Teil der Anweisungssequenz, die zu einem atomaren Ganzen zusammengefasst werden muss. In diesem Fall synchronizedwäre die Verwendung des Schlüsselworts daher wie folgt richtig :

public void setSpot (Point point) {// 'spot' setter log.println ("setSpot () aufgerufen" + this.toString ()); synchronisiert (dies) {this.x = point.x; this.y = point.y; }}

Der "faule" Weg und der Ansatz, den Sie vermeiden sollten, sieht folgendermaßen aus: