JavaProgrammierungSenior Java-Entwickler

Warum kommt die Happens-Before-Beziehung des Java-Speichermodells nicht für die Garantie der Unveränderlichkeit von finalen Feldern zum Tragen, wenn die Referenz 'this' während der Objekterstellung entkommt?

Bestehen Sie Vorstellungsgespräche mit dem Hintsage-KI-Assistenten

Antwort auf die Frage.

Das Java-Speichermodell (JMM) garantiert, dass sobald ein Konstruktor abgeschlossen ist, Schreibvorgänge in final Felder für jeden Thread sichtbar werden, der die Objektreferenz liest, vorausgesetzt, dass diese Referenz während der Konstruktion nicht entkommen ist. Wenn die this-Referenz vorzeitig entkommt – indem sie an einen anderen Thread übergeben oder in einer statischen Sammlung gespeichert wird, bevor der Konstruktor zurückkehrt – wird die Happens-Before-Beziehung zwischen dem Schreibvorgang des Konstruktors zum final Feld und dem Lesevorgang des anderen Threads unterbrochen. Folglich könnte der beobachtende Thread den Standardwert (null, false oder 0) anstelle des konstruierten Wertes sehen, was die scheinbare Unveränderlichkeit bricht. Eine sichere Veröffentlichung erfordert, dass keine Referenz auf das sich im Aufbau befindliche Objekt entkommt, bis die Konstruktion abgeschlossen ist, was sicherstellt, dass die Einfrieraktion auf final Felder erfolgt, bevor ein Thread die Referenz laden kann.

Lebenssituation

Wir begegneten diesem Problem in einem Hochfrequenzhandelssystem, in dem sich Service-Instanzen während ihrer Konstruktoren in einem globalen ConcurrentHashMap registrierten, um eine Suche zu erleichtern. Die Klasse definierte eine final long instrumentId, die aus einem Konstruktorparameter initialisiert wurde, dennoch lasen Überwachungsthreads sporadisch null, als sie das Register unmittelbar nach der Erstellung abfragten.

Eine vorgeschlagene Lösung war es, instrumentId anstelle von final als volatile zu deklarieren, in der Hoffnung, sofortige Sichtbarkeit über mehrere Kerne zu erzwingen. Dieser Ansatz garantierte Atomarität und Sichtbarkeit, opferte jedoch den Vertrauensschutz der Unveränderlichkeit und brachte die Kosten eines vollständigen Speicherbarrier auf jede Leseoperation mit sich, was den Durchsatz für einen Wert, der sich nach der Konstruktion niemals änderte, unnötig verringerte und die Überlegung über den Zustand des Objekts komplizierte.

Ein weiterer Vorschlag beinhaltete die Synchronisierung aller Zugriffe auf das Register mit synchronized-Blöcken, die die Logik des Konstruktors umschlossen, in der Überlegung, dass das Sperren die Speicherkopien leeren würde. Während dies Wettlaufbedingungen verhinderte, führte es zu starker Konkurrenz bei der globalen Registry-Sperre, wodurch eine nebenläufige Struktur in einen seriellen Engpass umgewandelt wurde und strenge Latenzanforderungen für die Markt-Datenaufnahme verletzt wurden.

Wir wählten ein Fabrikmuster, das die Instanziierung von der Registrierung entkoppelte. Der Konstruktor blieb privat, die Fabrikmethode rief new Service(id) vollständig auf, und erst danach veröffentlichte sie die vollständig gebaute Referenz im ConcurrentHashMap. Dies nutzte die Einfrier-Semantiken der finalen Felder des JMM ohne Synchronisationsüberkopf und stellte sicher, dass die instrumentId sofort nach dem Abruf sichtbar war.

Die Änderung beseitigte die Null-Sichtbarkeitsanomalien und stellte die erwartete Mikrosekunden-Latenz für die Servicerecherche wieder her, während das Designziel der Unveränderlichkeit erhalten blieb.

Was Kandidaten oft übersehen

Warum garantiert final keine Sichtbarkeit, wenn ich die Referenz einfach durch eine thread-sichere Sammlung wie ConcurrentHashMap veröffentliche?

Die happens-before-Beziehung, die von den Put- und Get-Operationen von ConcurrentHashMap bereitgestellt wird, stellt eine Reihenfolge zwischen den internen Zustandsänderungen der Map her, nicht zwischen den Schreibvorgängen des Konstruktors und der Veröffentlichung der Map. Wenn this während der Konstruktion entkommt, geschieht das Schreiben zum final Feld in einem Thread, während die Veröffentlichung der Map gleichzeitig erfolgt und die notwendige Happens-Before-Beziehung fehlt, um eine Anweisungsneuordnung zu verhindern. Daher könnte der lesende Thread die Referenz durch die Map sehen, bevor die Schreibvorgänge des Konstruktors in den Hauptspeicher gespült werden, und den Standardwert beobachten.

Kann ich das beheben, indem ich das Registrierungsfeld volatile anstelle der Felder des Objekts mache?

Das Kennzeichnen der Registrierungsreferenz als volatile stellt nur sicher, dass Änderungen an der Registrierungsvariable selbst sichtbar sind, nicht der interne Zustand der Objekte, die sie enthält. Da das Problem im Timing der Feldschreibvorgänge des Objekts relativ zur Sichtbarmachung der Referenz liegt, reicht volatile auf dem Container nicht aus, um die notwendige Reihenfolge zwischen dem Konstruktor und dem Verbraucher des Objekts herzustellen. Sie würden immer noch teilweise konstruierte Instanzen beobachten.

Verhindert die Verwendung von synchronized innerhalb des Konstruktors die unsichere Veröffentlichung?

Das Setzen von synchronized auf den Konstruktor oder die Verwendung davon zum Schützen der Registrierung verhindert, dass andere Threads gleichzeitig in den kritischen Abschnitt eintreten, verhindert jedoch nicht, dass die this-Referenz entkommt, wenn die Registrierungsfunktion die Referenz an einen Thread weitergibt, der außerhalb dieses Locks arbeitet. Das JMM verlangt ausdrücklich, dass keine Referenz auf das Objekt entkommt, bevor der Konstruktor die Fertigstellung erreicht, damit die Semantiken der finalen Felder gelten; Synchronisation ohne die richtige Publikationsreihenfolge kann dieses Garant nicht wiederherstellen.