Vermeiden Sie (dies) in Java synchronisiert?


381

Immer wenn auf SO eine Frage zur Java-Synchronisation auftaucht, möchten einige Leute darauf hinweisen, dass synchronized(this)dies vermieden werden sollte. Stattdessen sei eine Sperre einer privaten Referenz vorzuziehen.

Einige der angegebenen Gründe sind:

Andere Leute, einschließlich mir, argumentieren, dass synchronized(this)eine Redewendung, die häufig verwendet wird (auch in Java-Bibliotheken), sicher und gut verstanden ist. Es sollte nicht vermieden werden, da Sie einen Fehler haben und keine Ahnung haben, was in Ihrem Multithread-Programm vor sich geht. Mit anderen Worten: Wenn es anwendbar ist, dann benutze es.

Ich bin daran interessiert, einige Beispiele aus der Praxis zu sehen (kein Foobar-Zeug), bei denen thises vorzuziehen ist , eine Sperre zu vermeiden, wenn synchronized(this)dies auch der Fall ist.

Deshalb: sollten Sie es immer vermeiden synchronized(this)und durch eine Sperre für eine private Referenz ersetzen?


Einige weitere Informationen (aktualisiert, wenn Antworten gegeben werden):

  • Wir sprechen über Instanzsynchronisation
  • Es werden sowohl implizite ( synchronizedMethoden) als auch explizite Formen von synchronized(this)berücksichtigt
  • Wenn Sie Bloch oder andere Autoritäten zu diesem Thema zitieren, lassen Sie die Teile, die Sie nicht mögen, nicht aus (z. B. Effektives Java, Punkt zur Thread-Sicherheit: In der Regel ist dies die Sperre für die Instanz selbst, es gibt jedoch Ausnahmen.)
  • Wenn Sie eine andere Granularität als die bereitgestellte Sperre benötigen synchronized(this), synchronized(this)ist diese nicht anwendbar, sodass dies nicht das Problem ist

4
Ich möchte auch darauf hinweisen, dass der Kontext wichtig ist - das Bit "Normalerweise ist es die Sperre für die Instanz selbst" befindet sich in einem Abschnitt über die Dokumentation einer bedingt threadsicheren Klasse, wenn Sie die Sperre öffentlich machen. Mit anderen Worten, dieser Satz gilt, wenn Sie diese Entscheidung bereits getroffen haben.
Jon Skeet

In Ermangelung einer internen Synchronisierung und wenn eine externe Synchronisierung erforderlich ist, ist die Sperre häufig die Instanz selbst, sagt Bloch im Grunde. Warum sollte dies nicht auch für die interne Synchronisierung mit Sperre für "dies" der Fall sein? (Die Bedeutung der Dokumentation ist ein weiteres Problem.)
Eljenso

Es gibt einen Kompromiss zwischen erweiterter Granularität und zusätzlichem Overhead für CPU-Cache und Busanforderungen, da für das Sperren eines externen Objekts höchstwahrscheinlich eine separate Cache-Zeile erforderlich ist, die geändert und zwischen CPU-Caches ausgetauscht werden muss (vgl. MESIF und MOESI).
ArtemGr

1
Ich denke, in der Welt der defensiven Programmierung verhindern Sie Fehler nicht durch Redewendung, sondern durch Code. Wenn mir jemand die Frage stellt: "Wie optimiert ist Ihre Synchronisation?", Möchte ich "Sehr" anstelle von "Sehr" sagen, es sei denn, jemand anderes folgt nicht der Redewendung.
Sgene9

Antworten:


132

Ich werde jeden Punkt separat behandeln.

  1. Ein böser Code kann Ihr Schloss stehlen (sehr beliebt, hat auch eine "versehentliche" Variante)

    Ich mache mir aus Versehen mehr Sorgen . Es kommt darauf an, dass diese Verwendung thisTeil der exponierten Schnittstelle Ihrer Klasse ist und dokumentiert werden sollte. Manchmal ist die Fähigkeit eines anderen Codes zur Verwendung Ihres Schlosses erwünscht. Dies gilt für Dinge wie Collections.synchronizedMap(siehe Javadoc).

  2. Alle synchronisierten Methoden innerhalb derselben Klasse verwenden genau dieselbe Sperre, wodurch der Durchsatz verringert wird

    Das ist zu simpel gedacht; Nur loszuwerden synchronized(this)wird das Problem nicht lösen. Die richtige Synchronisation für den Durchsatz erfordert mehr Überlegungen.

  3. Sie legen (unnötigerweise) zu viele Informationen offen

    Dies ist eine Variante von # 1. Die Verwendung von synchronized(this)ist Teil Ihrer Benutzeroberfläche. Wenn Sie dies nicht ausgesetzt haben wollen / müssen, tun Sie es nicht.


1. "synchronisiert" ist nicht Teil der exponierten Schnittstelle Ihrer Klasse. 2. zustimmen 3. siehe 1.
eljenso

66
Im Wesentlichen synchronisiert (dies) wird angezeigt, da dies bedeutet, dass externer Code den Betrieb Ihrer Klasse beeinträchtigen kann. Ich behaupte also, dass Sie es als Schnittstelle dokumentieren müssen, auch wenn die Sprache dies nicht tut.
Darron

15
Ähnlich. Siehe Javadoc für Collections.synchronizedMap () - das zurückgegebene Objekt verwendet intern synchronized (this) und erwartet, dass der Verbraucher dies nutzt, um dieselbe Sperre für große atomare Operationen wie die Iteration zu verwenden.
Darron

3
Tatsächlich verwendet Collections.synchronizedMap () NICHT synchronized (this) intern, sondern ein privates Objekt für die endgültige Sperre.
Bas Leijdekkers

1
@Bas Leijdekkers: In der Dokumentation wird klar angegeben, dass die Synchronisierung auf der zurückgegebenen Karteninstanz erfolgt. Interessant ist, dass die Ansichten von zurückgegeben werden keySet()und values()sich nicht auf (ihre) this, sondern auf die Karteninstanz beschränken, was wichtig ist, um ein konsistentes Verhalten für alle Kartenoperationen zu erzielen. Der Grund dafür, dass das Sperrobjekt in eine Variable zerlegt wird, besteht darin, dass die Unterklasse SynchronizedSortedMapes zum Implementieren von Unterkarten benötigt , die die ursprüngliche Karteninstanz sperren.
Holger

86

Nun, zunächst sollte darauf hingewiesen werden, dass:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

ist semantisch äquivalent zu:

public synchronized void blah() {
  // do stuff
}

Das ist ein Grund, nicht zu verwenden synchronized(this). Sie könnten argumentieren, dass Sie Dinge rund um den synchronized(this)Block tun können . Der übliche Grund besteht darin, zu vermeiden, dass die synchronisierte Prüfung überhaupt durchgeführt werden muss, was zu allen möglichen Parallelitätsproblemen führt, insbesondere zu dem Problem der doppelten verriegelten Verriegelung , das nur zeigt, wie schwierig es sein kann, eine relativ einfache Prüfung durchzuführen threadsicher.

Ein privates Schloss ist ein Abwehrmechanismus, was niemals eine schlechte Idee ist.

Wie Sie bereits angedeutet haben, können private Sperren die Granularität steuern. Ein Satz von Operationen an einem Objekt kann völlig unabhängig von einem anderen sein, synchronized(this)schließt jedoch den Zugriff auf alle gegenseitig aus.

synchronized(this) gibt dir einfach wirklich nichts.


4
"synchronisiert (das) gibt dir einfach wirklich nichts." Ok, ich ersetze es durch eine Synchronisierung (myPrivateFinalLock). Was gibt mir das? Sie sprechen davon, dass es sich um einen Abwehrmechanismus handelt. Wofür bin ich geschützt?
Eljenso

14
Sie sind gegen versehentliches (oder böswilliges) Sperren von "Dies" durch externe Objekte geschützt.
Cletus

14
Ich stimme dieser Antwort überhaupt nicht zu: Eine Sperre sollte immer so kurz wie möglich gehalten werden, und genau aus diesem Grund möchten Sie einen synchronisierten Block "erledigen", anstatt die gesamte Methode zu synchronisieren .
Olivier

5
Dinge außerhalb des synchronisierten Blocks zu tun ist immer gut gemeint. Der Punkt ist, dass die Leute dies oft falsch verstehen und es nicht einmal bemerken, genau wie beim doppelt überprüften Verriegelungsproblem. Der Weg zur Hölle ist mit guten Absichten gepflastert.
Cletus

16
Ich bin im Allgemeinen nicht einverstanden mit "X ist ein Abwehrmechanismus, was niemals eine schlechte Idee ist." Aufgrund dieser Einstellung gibt es eine Menge unnötig aufgeblähten Codes.
Finnw

54

Während Sie synchronized (this) verwenden, verwenden Sie die Klasseninstanz als Sperre. Dies bedeutet, dass der Thread 2 warten sollte , während die Sperre von Thread 1 übernommen wird .

Angenommen, der folgende Code:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

Bei Methode 1, bei der die Variable a geändert wird, und bei Methode 2, bei der die Variable b geändert wird , sollte die gleichzeitige Änderung derselben Variablen durch zwei Threads vermieden werden. Aber während thread1 Modifizieren a und Thread2 modifizierende b kann ohne Race - Bedingung durchgeführt werden.

Leider lässt der obige Code dies nicht zu, da wir dieselbe Referenz für eine Sperre verwenden. Dies bedeutet, dass Threads warten sollten, auch wenn sie sich nicht in einem Race-Zustand befinden, und offensichtlich opfert der Code die Parallelität des Programms.

Die Lösung besteht darin, zwei verschiedene Sperren für zwei verschiedene Variablen zu verwenden:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

Das obige Beispiel verwendet feinkörnigere Sperren (2 Sperren statt einer ( lockA und lockB für die Variablen a bzw. b )) und ermöglicht dadurch eine bessere Parallelität. Andererseits wurde es komplexer als das erste Beispiel ...


Das ist sehr gefährlich. Sie haben jetzt eine clientseitige (Benutzer dieser Klasse) Sperranordnungsanforderung eingeführt. Wenn zwei Threads method1 () und method2 () in einer anderen Reihenfolge aufrufen, ist es wahrscheinlich, dass sie blockieren, aber der Benutzer dieser Klasse hat keine Ahnung, dass dies der Fall ist.
Daveb

7
Granularität, die nicht durch "synchronized (this)" bereitgestellt wird, fällt nicht in den Rahmen meiner Frage. Und sollten Ihre Sperrfelder nicht endgültig sein?
Eljenso

10
Um einen Deadlock zu haben, sollten wir einen Anruf von einem von A synchronisierten Block zu einem von B. daveb synchronisierten Block ausführen. Sie liegen falsch ...
Andreas Bakurov

3
Soweit ich sehen kann, gibt es in diesem Beispiel keinen Deadlock. Ich akzeptiere, dass es nur Pseudocode ist, aber ich würde eine der Implementierungen von java.util.concurrent.locks.Lock wie java.util.concurrent.locks.ReentrantLock
Shawn Vader

15

Obwohl ich damit einverstanden bin, dass ich mich nicht blind an dogmatische Regeln halte, erscheint Ihnen das Szenario "Schloss stehlen" so exzentrisch? Ein Thread könnte tatsächlich die Sperre für Ihr Objekt "extern" ( synchronized(theObject) {...}) erlangen und andere Threads blockieren, die auf synchronisierte Instanzmethoden warten.

Wenn Sie nicht an bösartigen Code glauben, denken Sie daran, dass dieser Code von Dritten stammen kann (z. B. wenn Sie einen Anwendungsserver entwickeln).

Die "zufällige" Version scheint weniger wahrscheinlich, aber wie sie sagen, "machen Sie etwas idiotensicher und jemand wird einen besseren Idioten erfinden".

Also stimme ich der Denkschule zu, die davon abhängt, was die Klasse macht.


Bearbeiten Sie die ersten 3 Kommentare von eljenso:

Ich habe das Problem des Schlossdiebstahls noch nie erlebt, aber hier ist ein imaginäres Szenario:

Angenommen, Ihr System ist ein Servlet-Container, und das Objekt, das wir in Betracht ziehen, ist die ServletContextImplementierung. Die getAttributeMethode muss threadsicher sein, da Kontextattribute gemeinsam genutzte Daten sind. so deklarieren Sie es als synchronized. Stellen wir uns auch vor, dass Sie einen öffentlichen Hosting-Service basierend auf Ihrer Container-Implementierung bereitstellen.

Ich bin Ihr Kunde und setze mein "gutes" Servlet auf Ihrer Site ein. Es kommt vor, dass mein Code einen Aufruf an enthält getAttribute.

Ein als anderer Kunde getarnter Hacker stellt sein bösartiges Servlet auf Ihrer Website bereit. Es enthält den folgenden Code in derinit Methode:

synchronisiert (this.getServletConfig (). getServletContext ()) {
   while (true) {}
}}

Angenommen, wir verwenden denselben Servlet-Kontext (gemäß der Spezifikation zulässig, solange sich die beiden Servlets auf demselben virtuellen Host befinden), ist mein Anruf für getAttributeimmer gesperrt. Der Hacker hat ein DoS für mein Servlet erreicht.

Dieser Angriff ist nicht möglich, wenn getAttribute auf einer privaten Sperre synchronisiert ist, da Code von Drittanbietern diese Sperre nicht erhalten kann.

Ich gebe zu, dass das Beispiel erfunden ist und eine vereinfachte Ansicht darüber, wie ein Servlet-Container funktioniert, aber meiner Meinung nach beweist es den Punkt.

Daher würde ich meine Entwurfsentscheidung aus Sicherheitsgründen treffen: Habe ich die vollständige Kontrolle über den Code, der Zugriff auf die Instanzen hat? Was wäre die Folge davon, wenn ein Thread eine Instanz auf unbestimmte Zeit sperrt?


es hängt davon ab, was die Klasse tut: Wenn es sich um ein 'wichtiges' Objekt handelt, dann private Referenz sperren? Andernfalls reicht das Sperren von Instanzen aus?
Eljenso

6
Ja, das Szenario des Schlossdiebstahls scheint mir weit hergeholt. Jeder erwähnt es, aber wer hat es tatsächlich getan oder erlebt? Wenn Sie ein Objekt "versehentlich" sperren, das Sie nicht sollten, gibt es einen Namen für diese Art von Situation: Es ist ein Fehler. Repariere es.
Eljenso

4
Das Sperren interner Referenzen ist auch nicht frei von dem "externen Synchronisierungsangriff": Wenn Sie wissen, dass ein bestimmter synchronisierter Teil des Codes auf das Auftreten eines externen Ereignisses wartet (z. B. Schreiben von Dateien, Wert in der Datenbank, Timer-Ereignis), können Sie dies wahrscheinlich tun dafür sorgen, dass es auch blockiert.
Eljenso

Lassen Sie mich gestehen, dass ich einer dieser Idioten bin, obwohl ich es getan habe, als ich jung war. Ich dachte, der Code sei sauberer, da kein explizites Sperrobjekt erstellt wurde, und verwendete stattdessen ein anderes privates Endobjekt, das für die Teilnahme am Monitor erforderlich war. Ich wusste nicht, dass das Objekt selbst eine Synchronisierung auf sich selbst durchführte. Sie können sich die folgende Hijinx vorstellen ...
Alan Cabrera

12

Es hängt von der Situation ab.
Wenn es nur eine oder mehrere Freigabeeinheiten gibt.

Das vollständige Arbeitsbeispiel finden Sie hier

Eine kleine Einführung.

Threads und gemeinsam nutzbare Entitäten
Es ist möglich, dass mehrere Threads auf dieselbe Entität zugreifen, z. B. mehrere connectionThreads, die eine einzige messageQueue gemeinsam nutzen. Da die Threads gleichzeitig ausgeführt werden, besteht möglicherweise die Möglichkeit, dass Daten von anderen überschrieben werden, was zu einer durcheinandergebrachten Situation führen kann.
Wir brauchen also eine Möglichkeit, um sicherzustellen, dass jeweils nur ein Thread auf eine gemeinsam nutzbare Entität zugreift. (KONKURRENZ).

Synchronisierter Block Der
synchronisierte () Block ist eine Möglichkeit, den gleichzeitigen Zugriff auf gemeinsam nutzbare Entitäten sicherzustellen.
Zunächst eine kleine Analogie
Angenommen, es gibt zwei Personen P1, P2 (Gewinde), ein Waschbecken (gemeinsam nutzbare Einheit) in einem Waschraum und eine Tür (Schloss).
Jetzt möchten wir, dass jeweils eine Person das Waschbecken benutzt.
Ein Ansatz besteht darin, die Tür mit P1 zu verriegeln, wenn die Tür verriegelt ist. P2 wartet, bis p1 seine Arbeit beendet hat.
P1 entriegelt die Tür,
dann kann nur p1 das Waschbecken benutzen.

Syntax.

synchronized(this)
{
  SHARED_ENTITY.....
}

"this" stellte die der Klasse zugeordnete intrinsische Sperre bereit (Java-Entwickler hat die Objektklasse so entworfen, dass jedes Objekt als Monitor fungieren kann). Der obige Ansatz funktioniert einwandfrei, wenn nur eine gemeinsam genutzte Entität und mehrere Threads (1: N) vorhanden sind. N gemeinsam nutzbare Einheiten - M Fäden Stellen Sie sich nun eine Situation vor, in der sich zwei Waschbecken in einem Waschraum und nur eine Tür befinden. Wenn wir den vorherigen Ansatz verwenden, kann jeweils nur p1 ein Waschbecken verwenden, während p2 draußen wartet. Es ist eine Verschwendung von Ressourcen, da niemand B2 (Waschbecken) verwendet. Ein klügerer Ansatz wäre, einen kleineren Raum im Waschraum zu schaffen und ihnen eine Tür pro Waschbecken zur Verfügung zu stellen. Auf diese Weise kann P1 auf B1 zugreifen und P2 kann auf B2 zugreifen und umgekehrt.
Geben Sie hier die Bildbeschreibung ein

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

Geben Sie hier die Bildbeschreibung ein
Geben Sie hier die Bildbeschreibung ein

Weitere Informationen zu Threads ----> finden Sie hier


11

In den C # - und Java-Lagern scheint diesbezüglich ein anderer Konsens zu bestehen. Der Großteil des Java-Codes, den ich gesehen habe, verwendet:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

Während sich der Großteil des C # -Codes für das wohl sicherere entscheidet:

// instance level lock object
private readonly object _syncObj = new object();

...

// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
    // do work here
}

Die C # -Sprache ist sicherlich sicherer. Wie bereits erwähnt, kann von außerhalb der Instanz kein böswilliger / versehentlicher Zugriff auf die Sperre erfolgen. Java-Code birgt ebenfalls dieses Risiko, aber es scheint, dass sich die Java-Community im Laufe der Zeit auf die etwas weniger sichere, aber etwas knappere Version konzentriert hat.

Das ist nicht als Ausgrabung gegen Java gedacht, sondern nur ein Spiegelbild meiner Erfahrung mit beiden Sprachen.


3
Vielleicht haben sie, da C # eine jüngere Sprache ist, aus schlechten Mustern gelernt, die im Java-Camp herausgefunden wurden, und solche Dinge besser codiert? Gibt es auch weniger Singletons? :)
Bill K

3
Er er. Möglicherweise wahr, aber ich werde mich nicht zum Köder erheben! Ein Gedanke, den ich mit
Sicherheit

1
Nur nicht wahr (um es schön
auszudrücken

7

Das java.util.concurrentPaket hat die Komplexität meines thread-sicheren Codes erheblich reduziert. Ich habe nur anekdotische Beweise, aber die meisten Arbeiten, mit denen ich gesehen habe, synchronized(x)scheinen darin zu bestehen, ein Schloss, ein Semaphor oder einen Latch erneut zu implementieren, aber die Monitore der unteren Ebene zu verwenden.

In diesem Sinne ist die Synchronisierung mit einem dieser Mechanismen analog zur Synchronisierung auf einem internen Objekt, anstatt eine Sperre zu verlieren. Dies ist insofern von Vorteil, als Sie absolut sicher sind, dass Sie den Eintritt in den Monitor durch zwei oder mehr Threads steuern.


6
  1. Machen Sie Ihre Daten unveränderlich, wenn es möglich ist ( finalVariablen)
  2. Wenn Sie die Mutation gemeinsam genutzter Daten über mehrere Threads hinweg nicht vermeiden können, verwenden Sie Programmierkonstrukte auf hoher Ebene [z. B. granulare LockAPI].

Eine Sperre bietet exklusiven Zugriff auf eine gemeinsam genutzte Ressource: Es kann jeweils nur ein Thread die Sperre erwerben, und für den gesamten Zugriff auf die gemeinsam genutzte Ressource muss die Sperre zuerst erworben werden.

Zu verwendender Beispielcode, ReentrantLockder die LockSchnittstelle implementiert

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

Vorteile der Sperre gegenüber synchronisiert (dies)

  1. Die Verwendung synchronisierter Methoden oder Anweisungen erzwingt, dass alle Sperrenerfassungen und -freigaben blockstrukturiert erfolgen.

  2. Lock-Implementierungen bieten zusätzliche Funktionen gegenüber der Verwendung synchronisierter Methoden und Anweisungen

    1. Ein nicht blockierender Versuch, eine Sperre zu erhalten ( tryLock())
    2. Ein Versuch, die Sperre zu erhalten, die unterbrochen werden kann ( lockInterruptibly())
    3. Ein Versuch, die Sperre zu erhalten, die eine Zeitüberschreitung verursachen kann ( tryLock(long, TimeUnit)).
  3. Eine Lock-Klasse kann auch ein Verhalten und eine Semantik bereitstellen, die sich erheblich von denen der impliziten Monitorsperre unterscheiden, z

    1. garantierte Bestellung
    2. nicht wiedereintretende Nutzung
    3. Deadlock-Erkennung

Schauen Sie sich diese SE-Frage zu verschiedenen Arten von Locks:

Synchronisation gegen Sperre

Sie können die Thread-Sicherheit erreichen, indem Sie anstelle der synchronisierten Blöcke die erweiterte Parallelitäts-API verwenden. Diese Dokumentation Seite bietet eine gute Programmierkonstrukte Thread - Sicherheit zu erreichen.

Sperrobjekte unterstützen Sperrsprachen, die viele gleichzeitige Anwendungen vereinfachen.

Ausführende definieren eine allgemeine API zum Starten und Verwalten von Threads. Von java.util.concurrent bereitgestellte Executor-Implementierungen bieten eine Thread-Pool-Verwaltung, die für umfangreiche Anwendungen geeignet ist.

Gleichzeitige Sammlungen erleichtern die Verwaltung großer Datensammlungen und können den Synchronisierungsbedarf erheblich reduzieren.

Atomic Variables verfügen über Funktionen, die die Synchronisation minimieren und Speicherkonsistenzfehler vermeiden.

ThreadLocalRandom (in JDK 7) bietet eine effiziente Generierung von Pseudozufallszahlen aus mehreren Threads.

Weitere Programmierkonstrukte finden Sie auch in den Paketen java.util.concurrent und java.util.concurrent.atomic .


5

Wenn Sie das entschieden haben:

  • Sie müssen das aktuelle Objekt sperren. und
  • Sie möchten es mit einer Granularität sperren, die kleiner als eine ganze Methode ist.

dann sehe ich kein tabu über synchronizezd (dies).

Einige Leute verwenden absichtlich synchronisiert (dies) (anstatt die Methode synchronisiert zu markieren) innerhalb des gesamten Inhalts einer Methode, weil sie denken, dass es für den Leser "klarer" ist, auf welchem ​​Objekt tatsächlich synchronisiert wird. Solange die Leute eine informierte Entscheidung treffen (z. B. verstehen, dass sie dadurch tatsächlich zusätzliche Bytecodes in die Methode einfügen und dies sich auf mögliche Optimierungen auswirken könnte), sehe ich kein besonderes Problem damit . Sie sollten immer das gleichzeitige Verhalten Ihres Programms dokumentieren, daher sehe ich das Argument "'synchronisiert' veröffentlicht das Verhalten" nicht als so überzeugend an.

In Bezug auf die Frage, welche Objektsperre Sie verwenden sollten, ist es meines Erachtens nichts Falsches, das aktuelle Objekt zu synchronisieren, wenn dies von der Logik Ihrer Arbeit und der typischen Verwendung Ihrer Klasse erwartet wird . Bei einer Sammlung ist das Objekt, von dem Sie logischerweise erwarten würden, dass es gesperrt wird, im Allgemeinen die Sammlung selbst.


1
"Wenn dies von der Logik erwartet würde ..." ist ein Punkt, den ich ebenfalls vermitteln möchte. Ich sehe keinen Sinn darin, immer private Schlösser zu verwenden, obwohl der allgemeine Konsens zu sein scheint, dass es besser ist, da es nicht weh tut und defensiver ist.
Eljenso

4

Ich denke, es gibt eine gute Erklärung dafür, warum jede dieser Techniken in einem Buch mit dem Titel Java Concurrency In Practice von Brian Goetz eine wichtige Rolle spielt. Er macht einen Punkt sehr deutlich - Sie müssen das gleiche Schloss "ÜBERALL" verwenden, um den Zustand Ihres Objekts zu schützen. Die synchronisierte Methode und die Synchronisierung eines Objekts gehen häufig Hand in Hand. ZB synchronisiert Vector alle seine Methoden. Wenn Sie ein Handle für ein Vektorobjekt haben und "setzen, wenn nicht vorhanden" ausführen, schützt Sie die bloße Vektorsynchronisierung der einzelnen Methoden nicht vor einer Beschädigung des Status. Sie müssen mit synchronized (vectorHandle) synchronisieren. Dies führt dazu, dass die gleiche Sperre von jedem Thread erfasst wird, der ein Handle für den Vektor hat, und schützt den Gesamtzustand des Vektors. Dies wird als clientseitige Sperre bezeichnet. Wir wissen, dass der Vektor tatsächlich alle seine Methoden synchronisiert (dies) / synchronisiert, und daher führt die Synchronisierung auf dem Objekt vectorHandle zu einer ordnungsgemäßen Synchronisation des Zustands der Vektorobjekte. Es ist dumm zu glauben, dass Sie threadsicher sind, nur weil Sie eine threadsichere Sammlung verwenden. Dies ist genau der Grund, warum ConcurrentHashMap die putIfAbsent-Methode explizit eingeführt hat, um solche Operationen atomar zu machen.

Zusammenfassend

  1. Die Synchronisierung auf Methodenebene ermöglicht das clientseitige Sperren.
  2. Wenn Sie ein privates Sperrobjekt haben, wird das clientseitige Sperren unmöglich. Dies ist in Ordnung, wenn Sie wissen, dass Ihre Klasse keine Funktionalität vom Typ "Put, wenn nicht vorhanden" hat.
  3. Wenn Sie eine Bibliothek entwerfen, ist es oft klüger, diese zu synchronisieren oder die Methode zu synchronisieren. Weil Sie selten in der Lage sind, zu entscheiden, wie Ihre Klasse verwendet wird.
  4. Hätte Vector ein privates Sperrobjekt verwendet, wäre es unmöglich gewesen, "in Abwesenheit" richtig zu stellen. Der Client-Code erhält niemals ein Handle für die private Sperre, wodurch die Grundregel der Verwendung des EXACT SAME LOCK zum Schutz seines Status verletzt wird.
  5. Die Synchronisierung mit dieser oder synchronisierten Methoden hat ein Problem, wie andere bereits betont haben - jemand könnte eine Sperre erhalten und diese niemals aufheben. Alle anderen Threads würden weiter darauf warten, dass die Sperre aufgehoben wird.
  6. Also wissen Sie, was Sie tun, und übernehmen Sie die richtige.
  7. Jemand argumentierte, dass ein privates Sperrobjekt eine bessere Granularität bietet - z. B. wenn zwei Vorgänge nicht miteinander zusammenhängen -, könnten sie durch unterschiedliche Sperren geschützt werden, was zu einem besseren Durchsatz führt. Aber ich denke, das ist Designgeruch und kein Codegeruch - wenn zwei Operationen völlig unabhängig sind, warum sind sie dann Teil der gleichen Klasse? Warum sollte ein Klassenclub überhaupt keine verwandten Funktionen haben? Kann eine Utility-Klasse sein? Hmmmm - einige nutzen die Manipulation von Zeichenfolgen und die Formatierung von Kalenderdaten über dieselbe Instanz? ... macht zumindest für mich keinen Sinn !!

3

Nein, das solltest du nicht immer . Ich neige jedoch dazu, dies zu vermeiden, wenn es bei einem bestimmten Objekt mehrere Bedenken gibt, die nur in Bezug auf sich selbst threadsicher sein müssen. Beispielsweise haben Sie möglicherweise ein veränderbares Datenobjekt mit den Feldern "label" und "parent". Diese müssen threadsicher sein, aber das Ändern des einen muss nicht verhindern, dass der andere geschrieben / gelesen wird. (In der Praxis würde ich dies vermeiden, indem ich die Felder als flüchtig deklariere und / oder die AtomicFoo-Wrapper von java.util.concurrent verwende.)

Die Synchronisierung ist im Allgemeinen etwas umständlich, da sie eine große Sperre auslöst, anstatt genau zu überlegen, wie Threads möglicherweise umeinander arbeiten dürfen. Das Verwenden synchronized(this)ist noch ungeschickter und unsozialer, da es heißt: "Niemand darf etwas ändern an dieser Klasse während ich das Schloss halte". Wie oft müssen Sie das tatsächlich tun?

Ich hätte viel lieber körnigere Schlösser; Selbst wenn Sie verhindern möchten, dass sich alles ändert (vielleicht serialisieren Sie das Objekt), können Sie einfach alle Sperren erwerben, um dasselbe zu erreichen. Außerdem ist dies auf diese Weise expliziter. Bei der Verwendung synchronized(this)ist nicht klar, warum Sie synchronisieren oder welche Nebenwirkungen auftreten können. Wenn Sie synchronized(labelMonitor)oder noch besser verwenden labelLock.getWriteLock().lock(), ist klar, was Sie tun und auf welche Auswirkungen Ihr kritischer Abschnitt beschränkt ist.


3

Kurze Antwort : Sie müssen den Unterschied verstehen und je nach Code eine Auswahl treffen.

Lange Antwort : Im Allgemeinen würde ich lieber versuchen, eine Synchronisierung (dies) zu vermeiden , um Konflikte zu reduzieren, aber private Sperren erhöhen die Komplexität, die Sie beachten müssen. Verwenden Sie also die richtige Synchronisation für den richtigen Job. Wenn Sie nicht so viel Erfahrung mit Multithread-Programmierung haben, würde ich mich lieber an die Instanzsperre halten und mich über dieses Thema informieren. (Das heißt: Nur die Verwendung von synchronize (this) macht Ihre Klasse nicht automatisch vollständig threadsicher.) Dies ist kein einfaches Thema, aber sobald Sie sich daran gewöhnt haben, ist die Antwort, ob Sie synchronize (this) verwenden oder nicht, selbstverständlich .


Verstehe ich Sie richtig, wenn Sie sagen, dass es von Ihrer Erfahrung abhängt?
Eljenso

Erstens hängt es von dem Code ab, den Sie schreiben möchten. Ich sage nur, dass Sie möglicherweise etwas mehr Erfahrung benötigen, wenn Sie umleiten, um die Synchronisierung (dies) nicht zu verwenden.
tcurdt

2

Eine Sperre wird entweder zur Sichtbarkeit oder zum Schutz einiger Daten vor gleichzeitigen Änderungen verwendet, die zu Rennen führen können.

Wenn Sie nur primitive Operationen ausführen müssen, um atomar zu sein, stehen Optionen wie AtomicIntegerund dergleichen zur Verfügung.

Angenommen, Sie haben zwei Ganzzahlen, die wie xund yKoordinaten miteinander in Beziehung stehen, die miteinander in Beziehung stehen und atomar geändert werden sollten. Dann würden Sie sie mit demselben Schloss schützen.

Eine Sperre sollte nur den Zustand schützen, der miteinander in Beziehung steht. Nicht weniger und nicht mehr. Wenn Sie synchronized(this)in jeder Methode verwenden, werden alle Threads selbst dann in Konflikt geraten, wenn der Status der Klasse nicht in Beziehung steht, selbst wenn der Status in Beziehung gesetzt wird.

class Point{
   private int x;
   private int y;

   public Point(int x, int y){
       this.x = x;
       this.y = y;
   }

   //mutating methods should be guarded by same lock
   public synchronized void changeCoordinates(int x, int y){
       this.x = x;
       this.y = y;
   }
}

Im obigen Beispiel habe ich nur ein Verfahren , das sowohl mutiert xund ynicht zwei verschiedene Verfahren , wie xund ymiteinander verknüpft , und , wenn ich zwei verschiedene Verfahren zum Mutieren gegeben hatte xund yseparat dann wäre es nicht gewesen threadsicher sein.

Dieses Beispiel soll nur demonstrieren und nicht unbedingt, wie es implementiert werden sollte. Der beste Weg, dies zu tun, wäre, es IMMUTABLE zu machen .

Im Gegensatz zum PointBeispiel gibt es ein Beispiel TwoCountersvon @Andreas, bei dem der Status, der durch zwei verschiedene Sperren geschützt wird, da der Status nicht miteinander in Beziehung steht.

Der Prozess der Verwendung verschiedener Sperren zum Schutz nicht verwandter Zustände wird als Lock Striping oder Lock Splitting bezeichnet


1

Der Grund , nicht zu synchronisieren auf das ist , dass manchmal braucht man mehr als ein Schloss (das zweite Schloss oft nach einigen zusätzlichen Denken entfernt wird, aber Sie es noch brauchen im Zwischenzustand). Wenn Sie sperren diese , müssen Sie immer daran denken , welche der beiden Schlösser ist dies ; Wenn Sie ein privates Objekt sperren, sagt Ihnen der Variablenname dies.

Aus Sicht des Lesers müssen Sie immer die beiden Fragen beantworten , wenn Sie feststellen, dass dies blockiert ist:

  1. welche Art von Zugriff durch geschützt dies ?
  2. Ist eine Sperre wirklich genug, hat nicht jemand einen Fehler eingeführt?

Ein Beispiel:

class BadObject {
    private Something mStuff;
    synchronized setStuff(Something stuff) {
        mStuff = stuff;
    }
    synchronized getStuff(Something stuff) {
        return mStuff;
    }
    private MyListener myListener = new MyListener() {
        public void onMyEvent(...) {
            setStuff(...);
        }
    }
    synchronized void longOperation(MyListener l) {
        ...
        l.onMyEvent(...);
        ...
    }
}

Wenn zwei Threads longOperation()in zwei verschiedenen Instanzen von beginnen BadObject, erhalten sie ihre Sperren. Wenn es Zeit zum Aufrufen ist l.onMyEvent(...), haben wir einen Deadlock, da keiner der Threads die Sperre des anderen Objekts erhalten kann.

In diesem Beispiel können wir den Deadlock beseitigen, indem wir zwei Sperren verwenden, eine für kurze Operationen und eine für lange.


2
Der einzige Weg, um in diesem Beispiel einen Deadlock zu erhalten, besteht darin, dass BadObjectA longOperationB aufruft , A's übergibt myListenerund umgekehrt. Nicht unmöglich, aber ziemlich verworren, was meine früheren Punkte unterstützt.
Eljenso

1

Wie hier bereits gesagt, kann der synchronisierte Block eine benutzerdefinierte Variable als Sperrobjekt verwenden, wenn die synchronisierte Funktion nur "dies" verwendet. Und natürlich können Sie mit Bereichen Ihrer Funktion manipulieren, die synchronisiert werden sollen und so weiter.

Aber jeder sagt, dass kein Unterschied zwischen synchronisierter Funktion und Block, der die gesamte Funktion abdeckt, "dies" als Sperrobjekt verwendet. Das ist nicht wahr, der Unterschied liegt im Bytecode, der in beiden Situationen generiert wird. Im Falle einer synchronisierten Blocknutzung sollte eine lokale Variable zugewiesen werden, die auf "this" verweist. Infolgedessen haben wir eine etwas größere Funktionsgröße (nicht relevant, wenn Sie nur wenige Funktionen haben).

Eine ausführlichere Erklärung des Unterschieds finden Sie hier: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Auch die Verwendung eines synchronisierten Blocks ist aus folgenden Gründen nicht gut:

Das synchronisierte Schlüsselwort ist in einem Bereich sehr begrenzt: Beim Verlassen eines synchronisierten Blocks müssen alle Threads, die auf diese Sperre warten, entsperrt werden, aber nur einer dieser Threads kann die Sperre übernehmen. Alle anderen sehen, dass die Sperre aufgehoben ist und kehren in den blockierten Zustand zurück. Das ist nicht nur eine Menge verschwendeter Verarbeitungszyklen: Oft beinhaltet der Kontextwechsel zum Entsperren eines Threads auch das Auslagern von Speicher von der Festplatte, und das ist sehr, sehr teuer.

Für weitere Informationen in diesem Bereich würde ich empfehlen, diesen Artikel zu lesen: http://java.dzone.com/articles/synchronized-considered


1

Dies ist eigentlich nur eine Ergänzung zu den anderen Antworten. Wenn Ihr Hauptargument gegen die Verwendung privater Objekte zum Sperren darin besteht, dass Ihre Klasse mit Feldern überfüllt ist, die nicht mit der Geschäftslogik zusammenhängen, muss Project Lombok @Synchronizeddas Boilerplate zur Kompilierungszeit generieren:

@Synchronized
public int foo() {
    return 0;
}

kompiliert zu

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}

0

Ein gutes Beispiel für die Verwendung synchronisiert (dies).

// add listener
public final synchronized void addListener(IListener l) {listeners.add(l);}
// remove listener
public final synchronized void removeListener(IListener l) {listeners.remove(l);}
// routine that raise events
public void run() {
   // some code here...
   Set ls;
   synchronized(this) {
      ls = listeners.clone();
   }
   for (IListener l : ls) { l.processEvent(event); }
   // some code here...
}

Wie Sie hier sehen können, verwenden wir die Synchronisierung, um auf einfache Weise (möglicherweise Endlosschleife der Ausführungsmethode) mit einigen synchronisierten Methoden dort zusammenzuarbeiten.

Natürlich kann es sehr einfach mit synchronisiertem privatem Feld umgeschrieben werden. Aber manchmal, wenn wir bereits ein Design mit synchronisierten Methoden haben (dh Legacy-Klasse, von der wir abgeleitet sind, kann synchronisiert (dies) die einzige Lösung sein).


Jedes Objekt kann hier als Sperre verwendet werden. Es muss nicht sein this. Es könnte ein privates Feld sein.
Finnw

Richtig, aber der Zweck dieses Beispiels war es, zu zeigen, wie eine ordnungsgemäße Synchronisierung durchgeführt wird, wenn wir uns für die Methodensynchronisierung entschieden haben.
Bart Prokop

0

Es hängt von der Aufgabe ab, die Sie erledigen möchten, aber ich würde sie nicht verwenden. Überprüfen Sie auch, ob die Thread-Speicherung, die Sie begleiten möchten, überhaupt nicht durch Synchronisieren (dieses) erfolgen kann. Es gibt auch einige nette Sperren in der API , die Ihnen helfen könnten :)


0

Ich möchte nur eine mögliche Lösung für eindeutige private Referenzen in atomaren Teilen des Codes ohne Abhängigkeiten erwähnen. Sie können eine statische Hashmap mit Sperren und eine einfache statische Methode namens atomic () verwenden, mit der erforderliche Referenzen automatisch mithilfe von Stapelinformationen (vollständiger Klassenname und Zeilennummer) erstellt werden. Anschließend können Sie diese Methode zum Synchronisieren von Anweisungen verwenden, ohne ein neues Sperrobjekt zu schreiben.

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}

0

Vermeiden Sie die Verwendung synchronized(this)als Sperrmechanismus: Dies sperrt die gesamte Klasseninstanz und kann Deadlocks verursachen. Refaktorieren Sie in solchen Fällen den Code so, dass nur eine bestimmte Methode oder Variable gesperrt wird. Auf diese Weise wird die gesamte Klasse nicht gesperrt. Synchronisedkann innerhalb der Methodenebene verwendet werden.
Anstatt zu verwenden synchronized(this), zeigt der folgende Code, wie Sie eine Methode einfach sperren können.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }

0

Meine zwei Cent im Jahr 2019, obwohl diese Frage bereits geklärt sein könnte.

Das Sperren von 'this' ist nicht schlecht, wenn Sie wissen, was Sie tun, aber hinter den Kulissen ist das Sperren von 'this' (was leider das synchronisierte Schlüsselwort in der Methodendefinition erlaubt).

Wenn Sie tatsächlich möchten, dass Benutzer Ihrer Klasse Ihre Sperre "stehlen" können (dh verhindern, dass andere Threads damit umgehen), möchten Sie, dass alle synchronisierten Methoden warten, während eine andere Synchronisierungsmethode ausgeführt wird, und so weiter. Es sollte absichtlich und gut durchdacht sein (und daher dokumentiert werden, damit Ihre Benutzer es besser verstehen).

Um dies näher zu erläutern, müssen Sie umgekehrt wissen, was Sie "gewinnen" (oder "verlieren"), wenn Sie ein nicht zugängliches Schloss sperren (niemand kann Ihr Schloss "stehlen", Sie haben die totale Kontrolle und so weiter. ..).

Das Problem für mich ist, dass das synchronisierte Schlüsselwort in der Signatur der Methodendefinition es Programmierern einfach zu einfach macht, nicht darüber nachzudenken, was sie sperren sollen. Dies ist eine sehr wichtige Sache, über die man nachdenken muss, wenn man in einem Multi nicht auf Probleme stoßen möchte -threaded Programm.

Man kann nicht behaupten, dass "normalerweise" Sie nicht möchten, dass Benutzer Ihrer Klasse diese Dinge tun können, oder dass "normalerweise" Sie möchten ... Es hängt davon ab, welche Funktionalität Sie codieren. Sie können keine Daumenregel erstellen, da Sie nicht alle Anwendungsfälle vorhersagen können.

Betrachten Sie zum Beispiel den Printwriter, der eine interne Sperre verwendet, aber die Leute Schwierigkeiten haben, diese von mehreren Threads aus zu verwenden, wenn sie nicht möchten, dass ihre Ausgabe verschachtelt wird.

Ob Ihre Sperre außerhalb der Klasse zugänglich ist oder nicht, liegt Ihre Entscheidung als Programmierer auf der Grundlage der Funktionalität der Klasse. Es ist Teil der API. Sie können beispielsweise nicht von synchronisiert (dies) zu synchronisiert (provateObjet) wechseln, ohne das Risiko einzugehen, dass Änderungen im Code, der ihn verwendet, beschädigt werden.

Anmerkung 1: Ich weiß, dass Sie alles erreichen können, was synchronisiert (dies) erreicht, indem Sie ein explizites Sperrobjekt verwenden und es verfügbar machen. Ich denke jedoch, dass es unnötig ist, wenn Ihr Verhalten gut dokumentiert ist und Sie tatsächlich wissen, was das Sperren von "dies" bedeutet.

Anmerkung 2: Ich stimme dem Argument nicht zu, dass ein Code, der versehentlich Ihr Schloss stiehlt, ein Fehler ist und Sie ihn lösen müssen. Dies ist in gewisser Weise das gleiche Argument wie die Aussage, dass ich alle meine Methoden öffentlich machen kann, auch wenn sie nicht öffentlich sein sollen. Wenn jemand "versehentlich" meine beabsichtigte private Methode aufruft, ist dies ein Fehler. Warum diesen Unfall überhaupt aktivieren !!! Wenn die Fähigkeit, Ihr Schloss zu stehlen, ein Problem für Ihre Klasse ist, lassen Sie es nicht zu. So einfach ist das.


-3

Ich denke, Punkte eins (jemand anderes, der Ihre Sperre verwendet) und zwei (alle Methoden, die dieselbe Sperre unnötig verwenden) können in jeder ziemlich großen Anwendung auftreten. Besonders wenn es keine gute Kommunikation zwischen Entwicklern gibt.

Es ist nicht in Stein gemeißelt, es geht hauptsächlich um bewährte Verfahren und die Vermeidung von Fehlern.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.