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).

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!

3voto

tcurdt Punkte 12170

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

Lange Antwort : Im Allgemeinen würde ich eher versuchen, Folgendes zu vermeiden synchronisieren(this) zur Verringerung von Konflikten, aber private Sperren erhöhen die Komplexität, derer man sich bewusst sein muss. Verwenden Sie also die richtige Synchronisierung für die richtige Aufgabe. Wenn Sie noch nicht so viel Erfahrung mit Multi-Thread-Programmierung haben, würde ich mich lieber an Instanzsperren halten und mich über dieses Thema informieren. (Abgesehen davon: Nur die Verwendung von synchronisieren(this) macht Ihre Klasse nicht automatisch vollständig thread-sicher). Dies ist kein einfaches Thema, aber wenn man sich einmal daran gewöhnt hat, ist die Antwort, ob man synchronisieren(this) oder nicht, ist ganz natürlich.

0 Stimmen

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

0 Stimmen

In erster Linie hängt es von dem Code ab, den Sie schreiben wollen. Ich sage nur, dass Sie vielleicht ein wenig mehr Erfahrung brauchen, wenn Sie davon absehen, synchronize(this) zu verwenden.

3voto

Andrzej Doyle Punkte 99892

Nein, das sollten Sie nicht つねに . Ich tendiere jedoch dazu, dies zu vermeiden, wenn es mehrere Anliegen für ein bestimmtes Objekt gibt, die nur in Bezug auf sich selbst thread-sicher sein müssen. Zum Beispiel könnte man ein veränderbares Datenobjekt haben, das "label"- und "parent"-Felder hat; diese müssen thread-sicher sein, aber wenn man eines ändert, muss das andere nicht vom Schreiben/Lesen blockiert werden. (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 im Allgemeinen ist etwas ungeschickt, da sie eine große Sperre auferlegt, anstatt genau zu überlegen, wie Threads umeinander herum arbeiten können. Verwendung von synchronized(this) ist noch ungeschickter und unsozialer, denn es heißt: "Niemand darf etwas ändern alles auf diese Klasse, während ich die Sperre halte". Wie oft müssen Sie das eigentlich tun?

Ich würde viel lieber mehr granulare Sperren haben; selbst wenn Sie wollen, um alles zu stoppen von ändern (vielleicht sind Sie das Objekt serialisieren), können Sie nur erwerben alle Sperren, um die gleiche Sache zu erreichen, plus es ist explizit, dass Weg. Wenn Sie synchronized(this) Es ist nicht klar, warum Sie synchronisieren oder was die Nebenwirkungen sein könnten. Wenn Sie synchronized(labelMonitor) oder noch besser labelLock.getWriteLock().lock() Es ist klar, was Sie tun und worauf sich die Auswirkungen Ihres kritischen Abschnitts beschränken.

1voto

Michael Punkte 37460

Dies ist eigentlich nur eine Ergänzung zu den anderen Antworten, aber wenn Ihr Haupteinwand gegen die Verwendung privater Objekte zum Sperren darin besteht, dass Ihre Klasse mit Feldern überladen wird, die nichts mit der Geschäftslogik zu tun haben, dann hat Project Lombok @Synchronized um das Boilerplate zur Kompilierzeit zu erzeugen:

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

kompiliert zu

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

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

1voto

18446744073709551615 Punkte 15274

Der Grund, nicht zu synchronisieren auf diese ist, dass man manchmal mehr als eine Sperre braucht (die zweite Sperre wird oft nach einigem Nachdenken entfernt, aber man braucht sie immer noch im Zwischenstadium). Wenn Sie sperren auf diese müssen Sie sich immer merken, welches der beiden Schlösser das richtige ist. diese Wenn Sie auf ein privates Objekt sperren, können Sie das am Variablennamen erkennen.

Aus der Sicht des Lesers, wenn Sie die Sperrung auf diese müssen Sie immer die beiden Fragen beantworten:

  1. welche Art von Zugang geschützt ist durch diese ?
  2. Ist eine Sperre wirklich genug, hat da nicht jemand einen Fehler eingebaut?

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 beginnen longOperation() auf zwei verschiedenen Instanzen von BadObject erwerben sie ihre Schlösser; wenn es an der Zeit ist, die l.onMyEvent(...) haben wir ein Deadlock, weil keiner der Threads die Sperre des anderen Objekts übernehmen kann.

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

2 Stimmen

Die einzige Möglichkeit, in diesem Beispiel in eine Sackgasse zu geraten, ist, wenn BadObject A ruft auf longOperation auf B, vorbei an A's myListener und vice versa. Nicht unmöglich, aber ziemlich verworren, was meine früheren Ausführungen bestätigt.

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