2016-05-10 5 views
52

Im folgenden Code:Warum ist 'synchronisiert (neues Objekt()) {} `ein No-Op?

class A { 
    private int number; 

    public void a() { 
     number = 5; 
    } 

    public void b() { 
     while(number == 0) { 
      // ... 
     } 
    } 
} 

Wenn Verfahren b und dann ein neuer Thread aufgerufen wird gestartet, welches Verfahren ein feuert, dann wird Verfahren b nicht immer sehen die Änderung der number garantiert und kann somit b nie enden .

Natürlich könnten wir machen numbervolatile dieses Problem zu beheben. Doch für die akademische Gründen nehmen wir an, dass volatile ist keine Option:

Die JSR-133 FAQs sagt uns:

Nachdem wir einen synchronisierten Block verlassen, lassen wir den Monitor, der die Wirkung der Spülung des Cache muss Hauptspeicher, so dass von diesem Thread vorgenommene Schreibvorgänge für andere Threads sichtbar sein können. Bevor wir einen synchronisierten Block eingeben können, erfassen wir den Monitor, der den lokalen Prozessor-Cache ungültig macht, so dass Variablen aus dem Hauptspeicher erneut geladen werden. Diese

klingt wie ich beide brauchen nur a und b zu betreten und verlassen jede synchronized -Block überhaupt, egal in welchem ​​Monitor die sie verwenden. Genauer gesagt, es klingt so ...:

class A { 
    private int number; 

    public void a() { 
     number = 5; 
     synchronized(new Object()) {} 
    } 

    public void b() { 
     while(number == 0) { 
      // ... 
      synchronized(new Object()) {} 
     } 
    } 
} 

... das Problem beseitigen würde und wird garantieren, dass b wird die Änderung a und damit wird auch schließlich enden sehen.

jedoch die FAQs auch deutlich Zustand:

Eine weitere Folge ist, dass das folgende Muster, das einige Leute Verwendung einer Speicherbarriere zu erzwingen, funktioniert nicht:

synchronized (new Object()) {} 

Dies ist eigentlich ein No-Op, und Ihr Compiler kann es vollständig entfernen, , weil der Compiler weiß, dass kein anderer Thread auf den gleichen Monitor synchronisieren wird. Sie müssen eine Vor-Hin-Beziehung für einen Thread einrichten, um die Ergebnisse eines anderen zu sehen.

Jetzt ist das verwirrend. Ich dachte, dass die synchronisierte Anweisung Caches zum Leeren bringen wird. Es kann sicherlich keinen Cache im Hauptspeicher löschen, so dass die Änderungen im Hauptspeicher nur von Threads gesehen werden können, die sich auf demselben Monitor synchronisieren, zumal für volatile, die im Grunde dasselbe tun, wir nicht einmal ein brauchen Monitor, oder irre ich mich dort? Also, warum ist das ein No-Op und verursacht nicht b durch Garantie zu beenden?

+2

Die Referenz wird nicht gespeichert, daher kann kein anderer Thread auf diese Referenz warten. Was versuchst du zu schützen? –

+14

Diese Frage illustriert perfekt den Grund, warum ich Leute dazu dränge, nicht zu versuchen, die Semantik einer Sprache in Bezug auf implementierungsspezifische Dinge zu erklären oder zu verstehen, die es gibt oder nicht gibt, wie einen fiktiven "lokalen Prozessor-Cache". –

+1

@DavidSchwartz Genau, und eigentlich die Phrase "Cache-Flushing", vor allem, wenn in der Nähe der Phrase "Hauptspeicher" kann sehr irreführend darüber sein, was tatsächlich passiert. Cache-Kohärenz-Protokolle gewährleisten häufig eine Speicherkonsistenz, ohne tatsächlich den langsamen Hauptspeicher zu erreichen. Und wenn Sie JLS und JMM ablehnen und versuchen, über die zugrundeliegende Architektur nachzudenken, müssen Sie Dinge wie die Tatsache berücksichtigen, dass einige Synchronisationsmechanismen die globale Speicherkonsistenz nicht gewährleisten (demonstriert durch IRIW). Also, wie yshavit gesagt hat, ist das Festhalten an der JLS der richtige Weg. –

Antwort

49

Die FAQ ist nicht die Autorität in der Sache; die JLS ist. Der Abschnitt 17.4.4 spezifiziert das Synchronisieren mit Beziehungen, die in Ereignisse vor Beziehungen eingehen (17.4.5). Der relevante Aufzählungspunkt ist:

  • Ein Entsperraktion auf Monitor msynchronisiert- mit allen nachfolgenden Verriegelungs Aktionen auf m (wobei „nachfolgenden“ definiert entsprechend der Synchronisations-Reihenfolge).

Seit m hier der Verweis auf die new Object() ist, und es ist nie einem anderen Thread gespeichert oder veröffentlicht, können wir sicher sein, dass kein anderer Thread eine Sperre auf m nach dem Schloss erwerben in diesem Block ist freigegeben. Darüber hinaus, da m ein neues Objekt ist, können wir sicher sein, dass es keine Aktion gibt, die zuvor darauf freigeschaltet wurde. Daher können wir sicher sein, dass keine Aktion formal mit dieser Aktion synchronisiert wird.

Technisch gesehen müssen Sie nicht einmal einen vollständigen Cache-Flush durchführen, um der JLS-Spezifikation zu entsprechen. es ist mehr als die JLS erfordert. Eine typische Implementierung tut das, weil es das einfachste ist, was die Hardware Ihnen ermöglicht, aber es geht sozusagen "darüber hinaus". In Fällen, in denen escape analysis einem optimierenden Compiler sagt, dass wir noch weniger benötigen, kann der Compiler weniger ausführen. In Ihrem Beispiel kann die Escape-Analyse dem Compiler mitteilen, dass die Aktion keine Auswirkung hat (aufgrund der obigen Überlegungen) und vollständig eliminiert werden kann.

+5

Das heißt, dass wir theoretisch den Cache überhaupt nicht leeren müssen. Aber wir müssen sicherstellen, dass ein anderer Thread, der auf demselben Monitor synchronisiert, alles bis zu dem Punkt sehen kann, an dem der vorherige Thread den synchronisierten Block verlassen hat, und der einfachste Weg, dies zu garantieren, besteht darin, den Lese-Cache auf enter und den Schreib-Cache einzuschalten Ausfahrt. Richtig? – yankee

+0

Yup, du hast es. – yshavit

+2

@yankee Was die FAQ sagt, plump und dumm IMO, ist, dass, wenn es einen solchen Cache gäbe, und das Leeren dieses Caches notwendig wäre, um Monitore so arbeiten zu lassen, wie sie funktionieren müssen, dann würde dieser Cache geleert werden. Warum das eine schlaue Aussage ist, weiß ich nicht. Es führt IMO zu viel mehr Missverständnis als Verständnis. –

20

folgendes Muster, das einige Leute verwenden, um eine Speicherbarriere zu erzwingen, funktioniert nicht:

Es ist ein No-op sein, nicht garantiert, aber die Spezifikation erlaubt es nicht zu sein, -op.Die Spezifikation erfordert nur dann eine Synchronisation, um eine "happen-before" -Beziehung zwischen zwei Threads herzustellen, wenn die beiden Threads für dasselbe Objekt synchronisiert werden. Es wäre jedoch einfacher, eine JVM zu implementieren, bei der die Identität des Objekts keine Rolle spielt.

Ich dachte, dass das synchronisierte-Statement Caches verursacht

Es gibt keine "Cache" in der Java Language Specification zu spülen. Das ist ein Konzept, das nur in den Details einiger Hardwareplattformen und JVM-Implementierungen (na ja, O. K., praktisch alle) existiert.

+1

Das bedeutet also, dass die Spezifikation es erlaubt, Synchronisationen zu verschieben, bis eine weitere Synchronisation auf dem gleichen Monitor stattfindet? – yankee

+1

> Es gibt keinen "Cache" in der Java-Sprachspezifikation " Eigentlich sagt die JLS Dinge wie" Der Compiler muss keine in Registern zwischengespeicherten Schreiboperationen löschen "in [Kapitel 17] (https://docs.oracle.com /javase/specs/jls/se7/html/jls-17.html).Klingt für mich so, als würden sie Register als Cache benutzen, also gibt es einen Cache in der JLS. – yankee

+1

@Yankee Das ist eine Erklärung in einem nicht-speichermodellbezogenen Teil der JLS. Wenn Sie die tatsächliche Definition des JMM lesen, finden Sie keinen Verweis auf einen Cache oder ein Register (außer möglicherweise in nicht normativen erklärenden Teilen). – Voo

Verwandte Themen