411 Stimmen

Vermeiden Sie synchronized(this) in Java?

Wann immer auf SO eine Frage zur Java-Synchronisierung auftaucht, sind einige Leute sehr eifrig dabei, darauf hinzuweisen, dass synchronized(this) sollte vermieden werden. Stattdessen sei eine Sperre für eine private Referenz vorzuziehen.

Einige der genannten Gründe sind:

Andere Leute, darunter auch ich, argumentieren, dass synchronized(this) ist ein Idiom, das häufig verwendet wird (auch in Java-Bibliotheken), sicher ist und gut verstanden wird. Es sollte nicht vermieden werden, weil Sie einen Fehler haben und keine Ahnung haben, was in Ihrem Multithreading-Programm vor sich geht. Mit anderen Worten: Wenn es anwendbar ist, dann verwenden Sie es.

Ich bin daran interessiert, einige Beispiele aus der Praxis zu sehen (kein Foobar-Zeug), bei denen die Vermeidung einer Sperre auf this ist vorzuziehen, wenn synchronized(this) würde die Aufgabe ebenfalls erfüllen.

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


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

  • es geht um die Synchronisierung von Instanzen
  • sowohl implizit ( synchronized Methoden) und explizite Form der synchronized(this) werden als
  • wenn Sie Bloch oder andere Autoritäten zu diesem Thema zitieren, lassen Sie nicht die Teile weg, die Ihnen nicht gefallen (z.B. Effektives Java, Artikel über Thread-Sicherheit: Normalerweise ist es die Sperre der Instanz selbst, aber es gibt Ausnahmen).
  • wenn Sie eine andere Granularität für Ihre Sperren benötigen als synchronized(this) bietet, dann synchronized(this) ist nicht anwendbar, also ist das nicht das Problem

4 Stimmen

Ich möchte auch darauf hinweisen, dass der Kontext wichtig ist - der Teil "Typischerweise ist es die Sperre der Instanz selbst" steht in einem Abschnitt über die Dokumentation einer bedingt thread-sicheren Klasse, wenn Sie die Sperre öffentlich machen. Mit anderen Worten, dieser Satz gilt, wenn Sie diese Entscheidung bereits getroffen haben.

0 Stimmen

In Ermangelung einer internen Synchronisierung und wenn eine externe Synchronisierung erforderlich ist, ist die Sperre oft die Instanz selbst, so Bloch. Warum sollte dies also nicht auch bei interner Synchronisation mit Sperre auf "this" der Fall sein? (Die Bedeutung der Dokumentation ist ein anderes Thema.)

0 Stimmen

Es besteht ein Kompromiss zwischen erweiterter Granularität und zusätzlichem CPU-Cache- und Busanforderungs-Overhead, da das Sperren eines externen Objekts höchstwahrscheinlich eine separate Cache-Zeile erfordert, die geändert und zwischen CPU-Caches ausgetauscht werden muss (vgl. MESIF und MOESI).

12voto

serg10 Punkte 30302

In den C#- und Java-Lagern scheint es diesbezüglich einen unterschiedlichen Konsens zu geben. Die Mehrheit des Java-Codes, den ich gesehen habe, verwendet:

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

während der Großteil des C#-Codes sich für die wohl sicherere Variante 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
}

Das C# Idiom ist sicherlich sicherer. Wie bereits erwähnt, kann kein böswilliger/unbeabsichtigter Zugriff auf die Sperre von außerhalb der Instanz erfolgen. Auch Java-Code birgt dieses Risiko, aber es scheint, dass die Java-Gemeinschaft im Laufe der Zeit zu der etwas weniger sicheren, aber dafür etwas knapperen Version übergegangen ist.

Das ist nicht als Kritik an Java gemeint, sondern spiegelt nur meine Erfahrungen mit beiden Sprachen wider.

3 Stimmen

Vielleicht, da C# eine jüngere Sprache ist, lernten sie von schlechten Mustern, die im Java-Lager herausgefunden wurden, und kodieren Dinge wie diese besser? Gibt es auch weniger Singletons? :)

5 Stimmen

He he. Möglicherweise ist das wahr, aber ich werde nicht auf den Köder hereinfallen! Was ich mit Sicherheit sagen kann, ist, dass es mehr Großbuchstaben im C#-Code gibt ;)

1 Stimmen

Das ist einfach nicht wahr (um es nett zu formulieren)

8voto

Ravindra babu Punkte 45577
  1. Machen Sie Ihre Daten unveränderlich, wenn dies möglich ist ( final Variablen)
  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 Lock API ]

Eine Sperre ermöglicht den exklusiven Zugriff auf eine gemeinsam genutzte Ressource: nur jeweils ein Thread kann die Sperre erwerben und jeder Zugriff auf die gemeinsam genutzte Ressource erfordert, dass die Sperre zuerst erworben wird.

Beispielcode zur Verwendung ReentrantLock die Folgendes implementiert Lock Schnittstelle

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

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

Vorteile von Lock gegenüber Synchronized(this)

  1. Die Verwendung von synchronisierten Methoden oder Anweisungen zwingt dazu, alle Sperranforderungen und -freigaben in einer blockstrukturierten Weise durchzuführen.

  2. Lock-Implementierungen bieten zusätzliche Funktionalität gegenüber der Verwendung von synchronisierten Methoden und Anweisungen durch die Bereitstellung von

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

    1. Garantierte Bestellung
    2. Verwendung von Nicht-Einsteigern
    3. Deadlock-Erkennung

Werfen Sie einen Blick auf diese SE-Frage zu verschiedenen Arten von Locks :

Synchronisierung vs. Sperre

Sie können Thread-Sicherheit erreichen, indem Sie anstelle von synchronisierten Blöcken die erweiterte Gleichzeitigkeits-API verwenden. Diese Dokumentation Seite bietet gute Programmierkonstrukte, um Threadsicherheit zu erreichen.

Objekte sperren unterstützen Locking Idioms, die viele nebenläufige Anwendungen vereinfachen.

Testamentsvollstrecker definieren eine High-Level-API für das Starten und Verwalten von Threads. Executor-Implementierungen, die von java.util.concurrent bereitgestellt werden, bieten eine Thread-Pool-Verwaltung, die für umfangreiche Anwendungen geeignet ist.

Concurrent Collections erleichtern die Verwaltung großer Datensammlungen und können den Bedarf an Synchronisierung erheblich reduzieren.

Atomare Variablen haben Funktionen, die die Synchronisierung minimieren und helfen, Speicherkonsistenzfehler zu vermeiden.

ThreadLocalRandom (in JDK 7) ermöglicht die effiziente Erzeugung von Pseudozufallszahlen aus mehreren Threads.

Siehe java.util.concurrent y java.util.concurrent.atomic Pakete auch für andere Programmierkonstrukte.

7voto

jamesh Punkte 19377

Die java.util.concurrent Paket hat die Komplexität meines thread-sicheren Codes erheblich reduziert. Ich kann mich nur auf Anekdoten stützen, aber die meisten Arbeiten, die ich mit synchronized(x) scheint die Neuimplementierung eines Locks, Semaphore oder Latch zu sein, aber unter Verwendung der untergeordneten Monitore.

In diesem Sinne ist die Synchronisierung mit einem dieser Mechanismen analog zur Synchronisierung auf ein internes Objekt, anstatt eine Sperre auslaufen zu lassen. Dies hat den Vorteil, dass Sie die absolute Gewissheit haben, dass Sie den Eintritt in den Monitor durch zwei oder mehr Threads kontrollieren.

5voto

Neil Coffey Punkte 21238

Wenn Sie das beschlossen haben:

  • Das, was Sie tun müssen, ist, sich auf das aktuelle Objekt; und
  • Sie wollen mit einer Granularität sperren, die kleiner ist als eine ganze Methode;

dann sehe ich nicht das Tabu über synchronizezd(this).

Manche Leute verwenden absichtlich synchronized(this) (anstatt die Methode als synchronized zu kennzeichnen) innerhalb des gesamten Inhalts einer Methode, weil sie denken, dass es "für den Leser klarer" ist, welches Objekt tatsächlich synchronisiert wird. Solange die Leute eine informierte Entscheidung treffen (z.B. verstehen, dass sie dadurch zusätzlichen Bytecode in die Methode einfügen und dies einen Dominoeffekt auf potentielle Optimierungen haben könnte), sehe ich darin kein besonderes Problem. Sie sollten das gleichzeitige Verhalten Ihres Programms immer dokumentieren, daher sehe ich das Argument "'synchronized' veröffentlicht das Verhalten" nicht als so zwingend an.

Was die Frage betrifft, welche Objektsperre Sie verwenden sollten, denke ich, dass nichts dagegen spricht, das aktuelle Objekt zu synchronisieren wenn dies nach der Logik dessen, was Sie tun, zu erwarten wäre und wie Ihre Klasse typischerweise verwendet werden würde . Bei einer Sammlung zum Beispiel ist das Objekt, von dem man logischerweise erwarten würde, dass es gesperrt wird, in der Regel die Sammlung selbst.

1 Stimmen

"Wenn dies nach der Logik zu erwarten wäre..." ist ein Punkt, den ich ebenfalls zu vermitteln versuche. Ich sehe nicht den Sinn von immer mit privaten Schlössern, obwohl der allgemeine Konsens zu sein scheint, dass es besser ist, da es nicht weh tut und defensiver ist.

4voto

Sandesh Sadhale Punkte 41

Ich denke, es gibt eine gute Erklärung, warum jede dieser Techniken unter Ihrem Gürtel unerlässlich sind, in einem Buch namens Java Concurrency In Practice von Brian Goetz. Er macht einen Punkt sehr deutlich - Sie müssen "ÜBERALL" die gleiche Sperre verwenden, um den Zustand Ihres Objekts zu schützen. Synchronisierte Methoden und die Synchronisierung auf ein Objekt gehen oft Hand in Hand. Ein Beispiel: Vector synchronisiert alle seine Methoden. Wenn Sie einen Handle auf ein Vektorobjekt haben und "put if absent" machen, dann schützt Sie die Synchronisierung der einzelnen Methoden von Vector nicht vor einer Verfälschung des Zustands. Sie müssen synchronisieren mit synchronised (vectorHandle). Dies führt dazu, dass jeder Thread, der ein Handle auf den Vektor hat, die GLEICHE Sperre erhält und den Gesamtzustand des Vektors schützt. Dies wird als clientseitiges Sperren bezeichnet. Wir wissen, dass vector synchronisiert (this) / synchronisiert alle seine Methoden und damit die Synchronisierung auf das Objekt vectorHandle führt zu einer ordnungsgemäßen Synchronisierung der Vektor-Objekte Zustand. Es ist töricht zu glauben, dass Sie Thread-sicher sind, nur weil Sie eine Thread-sichere Sammlung verwenden. Dies ist genau der Grund, warum ConcurrentHashMap ausdrücklich die putIfAbsent-Methode eingeführt hat - um solche Operationen atomar zu machen.

Zusammengefasst

  1. Die Synchronisierung auf Methodenebene ermöglicht clientseitiges Sperren.
  2. Wenn Sie ein privates Sperrobjekt haben, ist ein clientseitiges Sperren unmöglich. Dies ist in Ordnung, wenn Sie wissen, dass Ihre Klasse nicht "put if absent" Art von Funktionalität hat.
  3. Wenn Sie eine Bibliothek entwerfen, ist es oft klüger, diese zu synchronisieren oder die Methode zu synchronisieren. Denn Sie sind selten in der Lage zu entscheiden, wie Ihre Klasse verwendet werden soll.
  4. Hätte Vector ein privates Sperrobjekt verwendet, wäre es unmöglich gewesen, "put if absent" richtig zu machen. Der Client-Code wird niemals einen Zugriff auf die private Sperre erlangen und damit die Grundregel verletzen, die besagt, dass zum Schutz des Zustands die EXAKT SELBE SPERRE verwendet werden muss.
  5. Die Synchronisierung auf diese oder synchronisierte Methoden haben ein Problem, wie andere darauf hingewiesen haben - jemand könnte eine Sperre erhalten und sie nie freigeben. Alle anderen Threads würden darauf warten, dass die Sperre freigegeben wird.
  6. Sie sollten also wissen, was Sie tun, und sich für die richtige Lösung entscheiden.
  7. Jemand argumentierte, dass ein privates Sperrobjekt eine bessere Granularität bietet - z.B. wenn zwei Operationen nicht miteinander verbunden sind - könnten sie durch verschiedene Sperren geschützt werden, was zu einem besseren Durchsatz führt. Aber das ist meiner Meinung nach Design-Smell und nicht Code-Smell - wenn zwei Operationen nichts miteinander zu tun haben, warum sind sie dann Teil der gleichen Klasse? Warum sollte eine Klasse überhaupt unverbundene Funktionalitäten vereinen? Kann eine Utility-Klasse sein? Hmmmm - irgendeine Utility, die Stringmanipulation und Kalender-Datumsformatierung durch dieselbe Instanz bereitstellt? ... das ergibt zumindest für mich keinen Sinn!

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X