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

1voto

Roman Punkte 340

Wie schon gesagt, kann der synchronisierte Block eine benutzerdefinierte Variable als Lock-Objekt verwenden, während die synchronisierte Funktion nur "this" verwendet. Und natürlich können Sie mit den Bereichen Ihrer Funktion, die synchronisiert werden sollen, manipulieren und so weiter.

Aber jeder sagt, dass es keinen Unterschied zwischen einer synchronisierten Funktion und einem Block gibt, der die gesamte Funktion abdeckt und "this" als Lock-Objekt verwendet. Das ist nicht wahr, der Unterschied liegt im Bytecode, der in beiden Situationen erzeugt wird. Im Falle eines synchronisierten Blocks sollte eine lokale Variable verwendet werden, die einen Verweis auf "this" enthält. Das Ergebnis ist eine etwas größere Funktion (nicht relevant, wenn Sie nur eine geringe Anzahl von Funktionen haben).

Eine genauere 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 sinnvoll:

Das Schlüsselwort synchronized ist in einem Bereich sehr eingeschränkt: 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 übernommen wurde und kehren in den blockierten Zustand zurück. Das ist nicht nur eine Menge vergeudeter 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 Einzelheiten zu diesem Thema empfehle ich Ihnen diesen Artikel: http://java.dzone.com/articles/synchronized-considered

0voto

surendrapanday Punkte 394

Vermeiden Sie die Verwendung von synchronized(this) als Sperrmechanismus: Dies sperrt die gesamte Klasseninstanz und kann zu Deadlocks führen. In solchen Fällen sollte der Code so umgestaltet werden, dass nur eine bestimmte Methode oder Variable gesperrt wird, so dass nicht die gesamte Klasse gesperrt wird. Synchronised kann innerhalb der Methodenebene verwendet werden.
Statt der Verwendung von synchronized(this) Der folgende Code zeigt, wie Sie eine Methode sperren können.

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

0voto

Bart Prokop Punkte 412

Ein gutes Beispiel für die Verwendung von synchronized(this).

// 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 synchronize auf diese zu einfach kooperieren von langwierigen (möglicherweise Endlosschleife von run-Methode) mit einigen synchronisierten Methoden gibt.

Natürlich kann es sehr einfach umgeschrieben werden, indem man synchronized on private field verwendet. Aber manchmal, wenn wir bereits ein Design mit synchronisierten Methoden haben (z.B. eine Legacy-Klasse, von der wir ableiten, kann synchronized(this) die einzige Lösung sein).

0 Stimmen

Hier kann jedes beliebige Objekt als Schloss verwendet werden. Es muss nicht sein this . Es könnte ein privates Feld sein.

0 Stimmen

Richtig, aber der Zweck dieses Beispiels war es, zu zeigen, wie man richtig synchronisiert, wenn man sich für die Methodensynchronisation entscheidet.

0voto

Amit Mittal Punkte 2506

Meine zwei Cents im Jahr 2019, auch wenn diese Frage bereits geklärt sein könnte.

Sperren auf "this" ist nicht schlecht, wenn Sie wissen, was Sie tun, aber hinter der Szene Sperren auf "this" ist (was leider, was synchronisierte Schlüsselwort in Methodendefinition ermöglicht).

Wenn Sie tatsächlich wollen, dass Benutzer Ihrer Klasse Ihre Sperre "stehlen" können (d.h. andere Threads daran hindern, damit umzugehen), dann wollen Sie, dass alle synchronisierten Methoden warten, während eine andere Sync-Methode ausgeführt wird usw. Dies sollte absichtlich und gut durchdacht sein (und daher dokumentiert werden, damit Ihre Benutzer es verstehen).

Im Umkehrschluss müssen Sie wissen, was Sie "gewinnen" (oder "verlieren"), wenn Sie auf ein nicht zugängliches Schloss schließen (niemand kann Ihr Schloss "stehlen", Sie haben die totale Kontrolle und so weiter...).

Das Problem für mich ist, dass das Schlüsselwort "synchronized" in der Signatur der Methodendefinition es den Programmierern zu leicht macht nicht zu denken auf was gesperrt werden soll, was sehr wichtig ist, wenn man in einem Multithreading-Programm keine Probleme bekommen will.

Man kann nicht argumentieren, dass man "typischerweise" nicht will, dass die Benutzer Ihrer Klasse diese Dinge tun können, oder dass man "typischerweise" will, dass... Es hängt davon ab, welche Funktionen Sie programmieren. Sie können keine Faustregel aufstellen, da Sie nicht alle Anwendungsfälle vorhersagen können.

Denken Sie z.B. an den Drucker, der eine interne Sperre verwendet, aber es ist schwierig, ihn von mehreren Threads aus zu benutzen, wenn die Ausgabe nicht verschachtelt werden soll.

Ob Ihr Schloss außerhalb der Klasse zugänglich sein soll oder nicht, entscheiden Sie als Programmierer auf der Grundlage der Funktionalität der Klasse. Es ist ein Teil der API. Sie können z. B. nicht von synchronized(this) zu synchronized(provateObjet) wechseln, ohne zu riskieren, dass der Code, der es verwendet, verändert wird.

Anmerkung 1: Ich weiß, dass Sie erreichen können, was synchronized(this) "erreicht", indem Sie ein explizites Sperrobjekt verwenden und es offenlegen, aber ich denke, es ist unnötig, wenn Ihr Verhalten gut dokumentiert ist und Sie tatsächlich wissen, was Sperren auf "this" bedeutet.

Anmerkung 2: Ich stimme nicht mit dem Argument überein, dass es ein Fehler ist, wenn ein Code versehentlich deine Sperre stiehlt und du ihn lösen musst. Das ist in gewisser Weise das gleiche Argument, wie wenn ich sagen würde, ich kann alle meine Methoden öffentlich machen, auch wenn sie nicht öffentlich sein sollen. Wenn jemand "versehentlich" meine eigentlich private Methode aufruft, ist das ein Fehler. Warum sollte man diesen Fehler überhaupt erst ermöglichen!!! Wenn die Möglichkeit, Ihr Schloss zu stehlen, ein Problem für Ihre Klasse ist, dann lassen Sie es nicht zu. So einfach ist das.

0voto

HKTonyLee Punkte 2678

Die Synchronisierung umfasst 3 Teile: Atomarität, Sichtbarkeit und Ordnung

Der synchronisierte Block ist eine sehr grobe Stufe der Synchronisierung. Er erzwingt die Sichtbarkeit und die Reihenfolge genau so, wie Sie es erwartet haben. Für Atomarität bietet er jedoch keinen großen Schutz. Atomarität erfordert eher globales Wissen über das Programm als lokales Wissen. (Und das macht Multithreading-Programmierung sehr schwierig)

Nehmen wir an, wir haben eine Klasse Account mit Methode deposit y withdraw . Sie werden beide auf der Grundlage einer privaten Sperre wie dieser synchronisiert:

class Account {
    private Object lock = new Object();

    void withdraw(int amount) {
        synchronized(lock) {
            // ...
        }
    }

    void deposit(int amount) {
        synchronized(lock) {
            // ...
        }
    }
}

In Anbetracht der Tatsache, dass wir eine übergeordnete Klasse implementieren müssen, die den Transfer abwickelt, etwa so:

class AccountManager {
    void transfer(Account fromAcc, Account toAcc, int amount) {
        if (fromAcc.getBalance() > amount) {
            fromAcc.setBalance(fromAcc.getBalance() - amount);
            toAcc.setBalance(toAcc.getBalance + amount);
        }
    }
}

Angenommen, wir haben jetzt 2 Konten,

Account john;
Account marry;

Wenn die Account.deposit() y Account.withdraw() sind nur mit dem internen Schloss verriegelt. Das wird ein Problem verursachen, wenn 2 Threads arbeiten:

// Some thread
void threadA() {
    john.withdraw(500);
}

// Another thread
void threadB() {
    accountManager.transfer(john, marry, 100);
}

Denn es ist möglich, dass sowohl threadA y threadB gleichzeitig laufen. Und Thread B beendet die Bedingungsprüfung, Thread A hebt ab und Thread B hebt wieder ab. Das bedeutet, dass wir $100 von John abheben können, auch wenn sein Konto nicht genug Geld hat. Dadurch wird die Atomarität gebrochen.

Sie können vorschlagen, dass: warum nicht hinzufügen withdraw() y deposit() zu AccountManager dann? Aber nach diesem Vorschlag müssen wir ein sicheres Multi-Thread-Programm erstellen Map die von verschiedenen Konten auf ihre Sperren verweisen. Wir müssen die Sperre nach der Ausführung löschen (sonst geht Speicher verloren). Und wir müssen auch sicherstellen, dass kein anderer auf die Account.withdraw() direkt. Dies führt zu einer Reihe von subtilen Fehlern.

Der korrekte und idiomatischste Weg ist, das Schloss in der Account . Und lassen Sie die AccountManager um das Schloss zu benutzen. Aber warum in diesem Fall nicht einfach das Objekt selbst verwenden?

class Account {
    synchronized void withdraw(int amount) {
        // ...
    }

    synchronized void deposit(int amount) {
        // ...
    }
}

class AccountManager {
    void transfer(Account fromAcc, Account toAcc, int amount) {
        // Ensure locking order to prevent deadlock
        Account firstLock = fromAcc.hashCode() < toAcc.hashCode() ? fromAcc : toAcc;
        Account secondLock = fromAcc.hashCode() < toAcc.hashCode() ? toAcc : fromAcc;

        synchronized(firstLock) {
            synchronized(secondLock) {
                if (fromAcc.getBalance() > amount) {
                    fromAcc.setBalance(fromAcc.getBalance() - amount);
                    toAcc.setBalance(toAcc.getBalance + amount);
                }
            }
        }
    }
}

In einfachen Worten ausgedrückt, funktioniert die private Sperre nicht bei etwas komplizierteren Multithreading-Programmen.

(Wiederveröffentlicht von https://stackoverflow.com/a/67877650/474197 )

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