Worauf sollte ich in Java-kritischen Abschnitten synchronisieren?

73

In Java ist die idiomatische Methode zum Deklarieren kritischer Abschnitte im Code die folgende:

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

Fast alle Blöcke werden synchronisiert this, aber gibt es dafür einen bestimmten Grund? Gibt es noch andere Möglichkeiten? Gibt es Best Practices für das zu synchronisierende Objekt? (wie private Instanzen von Object?)

Lindelof
quelle

Antworten:

50

Beachten Sie zunächst, dass die folgenden Codefragmente identisch sind.

public void foo() {
    synchronized (this) {
        // do something thread-safe
    }
}

und:

public synchronized void foo() {
    // do something thread-safe
}

mach genau das gleiche . Keine Präferenz für einen von ihnen außer der Lesbarkeit und dem Stil des Codes.

Wenn Sie Methoden oder Codeblöcke synchronisieren, ist es wichtig zu wissen, warum Sie so etwas tun und welches Objekt genau Sie sperren und zu welchem ​​Zweck .

Beachten Sie auch, dass es Situationen gibt, in denen Sie clientseitige Codeblöcke synchronisieren möchten, in denen der von Ihnen angeforderte Monitor (dh das synchronisierte Objekt) nicht unbedingt thiswie in diesem Beispiel ist:

Vector v = getSomeGlobalVector();
synchronized (v) {
    // some thread-safe operation on the vector
}

Ich schlage vor, Sie erhalten mehr Wissen über die gleichzeitige Programmierung. Sobald Sie genau wissen, was sich hinter den Kulissen abspielt, wird dies Ihnen sehr helfen. Sie sollten sich Concurrent Programming in Java ansehen , ein großartiges Buch zu diesem Thema. Wenn Sie schnell in das Thema eintauchen möchten, lesen Sie Java Concurrency @ Sun.

Yuval Adam
quelle
1
Die Code-Schnipsel machen logischerweise das Gleiche, aber sie kompilieren zu unterschiedlichem Bytecode !!
Neil Coffey
1
Sie sind nicht gleichwertig. Siehe cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
Jason Day
2
@jason Auf ein Dokument über DCL zu verweisen, weil "es darauf hinweist, dass der Compiler oder JMM unerwartete Dinge tun kann", ist übertrieben und nicht wirklich auf den Punkt. Sie müssen angeben, WARUM sie nicht gleichwertig sind. Die Mehrheit der Menschen stimmt zu, dass sie gleichwertig sind: stackoverflow.com/questions/417285
eljenso
2
Beachten Sie, dass eine synkronisierte Methode häufig nicht die beste Option ist, da wir die gesamte Zeit über eine Sperre halten. Wenn es zeitaufwändige, aber threadsichere Teile und ein nicht so zeitaufwändiges threadsicheres Teil enthält, ist eine synchronisierte Methode sehr falsch.
Hans-Peter Störr
4
Der offensichtlichste Unterschied besteht darin, dass ein synchronisierter (dieser) Block zu einem Bytecode kompiliert wird, der länger als eine synchronisierte Methode ist. Wenn Sie eine synchronisierte Methode schreiben, setzt der Compiler nur ein Flag auf die Methode und die JVM erhält die Sperre, wenn sie das Flag sieht. Wenn Sie einen synchronisierten (diesen) Block verwenden, generiert der Compiler einen Bytecode, der einem Try-finally-Block ähnelt, der die Sperre erfasst, aufhebt und in Ihre Methode einfügt.
Andrew
70

Wie frühere Antwortende festgestellt haben, ist es empfehlenswert, auf einem Objekt mit begrenztem Umfang zu synchronisieren (mit anderen Worten, wählen Sie den restriktivsten Bereich aus, mit dem Sie durchkommen können, und verwenden Sie diesen). Insbesondere ist das Synchronisieren auf thiseine schlechte Idee, es sei denn Sie möchten den Benutzern Ihrer Klasse erlauben, die Sperre zu erhalten.

Ein besonders hässlicher Fall tritt jedoch auf, wenn Sie sich für die Synchronisierung auf a entscheiden java.lang.String. Saiten können (und werden in der Praxis fast immer) interniert werden. Das bedeutet, dass sich jede Zeichenfolge mit gleichem Inhalt - in der GESAMTEN JVM - hinter den Kulissen als dieselbe Zeichenfolge herausstellt. Das bedeutet, dass, wenn Sie eine Zeichenfolge synchronisieren, ein anderer (völlig unterschiedlicher) Codeabschnitt, der auch eine Zeichenfolge mit demselben Inhalt sperrt, Ihren Code tatsächlich sperrt.

Ich habe einmal einen Deadlock in einem Produktionssystem behoben und (sehr schmerzhaft) den Deadlock auf zwei völlig unterschiedliche Open Source-Pakete verfolgt, die jeweils auf einer Instanz von String synchronisiert wurden, deren Inhalt beide waren "LOCK".

Jared
quelle
17
+1 für die hilfreiche Anekdote aus der Praxis über die String-Instanzsperre.
Oger Psalm33
1
Strings werden nur interniert, wenn sie absichtlich von intern () interniert wurden (exotischer Fall) oder in Code geschrieben sind
Danubian Sailor
43

Ich versuche, eine Synchronisierung zu vermeiden, thisda dadurch jeder von außen, der einen Verweis auf dieses Objekt hatte, meine Synchronisierung blockieren kann. Stattdessen erstelle ich ein lokales Synchronisationsobjekt:

public class Foo {
    private final Object syncObject = new Object();
    …
}

Jetzt kann ich dieses Objekt für die Synchronisation verwenden, ohne befürchten zu müssen, dass jemand das Schloss „stiehlt“.

Bombe
quelle
9
Und wer würde versuchen, Ihre Synchronisation auf Ihren Objekten so zu blockieren, dass eine Synchronisation auf diesen gefährlich, unpraktisch oder unbrauchbar wird?
Eljenso
8
Um Stapelspuren besser lesbar zu machen, möchten Sie der Klasse des gesperrten Objekts möglicherweise einen Namen geben, der angibt, um welche Sperre es sich handelt : private static class Lock { }; private final Object lock = new Lock();.
Tom Hawtin - Tackline
6
Wenn Ihr Programm Fehler enthält, debuggen Sie. Ich bin sehr skeptisch gegenüber der Tatsache, dass das Vermeiden von Synchronisation (dies) Ihnen helfen wird, wenn Sie bereits keine wirkliche Ahnung haben, was in Ihrem Multithread-Programm vor sich geht.
Eljenso
2
@eljenso - ich concu. Wenn Sie "this" aktivieren, kann ein externer Aufrufer mehrere Methodenaufrufe "atomisieren", was von unschätzbarem Wert sein kann. Erwägen Sie, eine synchronisierte Sammlung zu iterieren. oder ist es der Schmerz, dass PrintWriter, die eine private Sperre verwenden, jemals versucht haben, eine Stapelverfolgung ohne Unterbrechung zu schreiben?
Lawrence Dol
5
Ein weiteres Problem beim blinden Sperren von "Dies" besteht darin, dass mehrere Methoden mit derselben Sperre konkurrieren können, die sich logischerweise nicht gegenseitig ausschließen müssen.
sk.
6

Nur um hervorzuheben, dass in Java auch ReadWriteLocks verfügbar sind, die als java.util.concurrent.locks.ReadWriteLock zu finden sind.

In den meisten Fällen trenne ich meine Sperren als "zum Lesen" und "für Updates". Wenn Sie einfach ein synchronisiertes Schlüsselwort verwenden, werden alle Lesevorgänge für denselben Methoden- / Codeblock in die Warteschlange gestellt. Es kann immer nur ein Thread gleichzeitig auf den Block zugreifen.

In den meisten Fällen müssen Sie sich keine Gedanken über Parallelitätsprobleme machen, wenn Sie nur lesen. Wenn Sie schreiben, machen Sie sich Sorgen über gleichzeitige Aktualisierungen (was zu Datenverlust führt) oder über das Lesen während eines Schreibvorgangs (teilweise Aktualisierungen), über die Sie sich Sorgen machen müssen.

Daher ist eine Lese- / Schreibsperre für mich bei der Multithread-Programmierung sinnvoller.

Kent Lai
quelle
ReadWriteLocks haben nicht immer eine gute Leistung .
Shelby Moore III
4

Sie möchten ein Objekt synchronisieren, das als Mutex dienen kann. Wenn die aktuelle Instanz ( diese Referenz) geeignet ist (z. B. kein Singleton), können Sie sie verwenden, da in Java jedes Objekt als Mutex dienen kann.

In anderen Fällen möchten Sie möglicherweise einen Mutex für mehrere Klassen freigeben, wenn Instanzen dieser Klassen möglicherweise alle Zugriff auf dieselben Ressourcen benötigen.

Dies hängt stark von der Umgebung ab, in der Sie arbeiten, und von der Art des Systems, das Sie erstellen. In den meisten Java EE-Anwendungen, die ich gesehen habe, besteht eigentlich kein wirklicher Synchronisationsbedarf ...

Elektrischer Mönch
quelle
4

Persönlich denke ich, dass die Antworten, die darauf bestehen, dass die Synchronisierung nie oder nur selten richtig ist, thisfalsch sind. Ich denke, es hängt von Ihrer API ab. Wenn Ihre Klasse eine threadsichere Implementierung ist und Sie dies dokumentieren, sollten Sie verwenden this. Wenn durch die Synchronisierung nicht jede Instanz der Klasse als Ganzes beim Aufrufen ihrer öffentlichen Methoden threadsicher gemacht werden soll, sollten Sie ein privates internes Objekt verwenden. Wiederverwendbare Bibliothekskomponenten fallen häufig in die erstere Kategorie. Sie müssen sorgfältig überlegen, bevor Sie dem Benutzer nicht erlauben, Ihre API in eine externe Synchronisierung einzuschließen.

Im ersteren Fall können mit using thismehrere Methoden atomar aufgerufen werden. Ein Beispiel ist PrintWriter, bei dem Sie möglicherweise mehrere Zeilen ausgeben möchten (z. B. eine Stapelverfolgung zur Konsole / zum Logger) und sicherstellen möchten, dass sie zusammen angezeigt werden. In diesem Fall ist die Tatsache, dass das Synchronisierungsobjekt intern ausgeblendet wird, ein echtes Problem. Ein weiteres Beispiel sind die synchronisierten Auflistungs-Wrapper. Dort müssen Sie das Auflistungsobjekt selbst synchronisieren, um iterieren zu können. Da die Iteration aus mehreren Methodenaufrufen besteht, können Sie sie intern nicht vollständig schützen.

Im letzteren Fall verwende ich ein einfaches Objekt:

private Object mutex=new Object();

Nachdem ich jedoch viele JVM-Dumps und Stack-Traces gesehen habe, die besagen, dass eine Sperre "eine Instanz von java.lang.Object ()" ist, muss ich sagen, dass die Verwendung einer inneren Klasse oft hilfreicher ist, wie andere vorgeschlagen haben.

Jedenfalls sind das meine zwei Bits wert.

Bearbeiten: Eine andere Sache, wenn thisich synchronisiere, ziehe ich es vor, die Methoden zu synchronisieren und die Methoden sehr detailliert zu halten. Ich denke, es ist klarer und prägnanter.

Lawrence Dol
quelle
1
Das Sperren gegen eine java.lang.String ist eine schlechte Idee - wenn Strings interniert werden. Das Ergebnis ist, dass anderer völlig unterschiedlicher Code (unbeabsichtigt) Ihre Sperre sperren kann. Es ist durchaus vernünftig, sich auf eine innere Klasse zu beschränken, deren Name beschreibend ist. +1, wenn dieser Vorschlag herausgeschnitten wird.
Jared
@Jared: Beachten Sie, dass ich einen neuen String ("xxx") angegeben habe, nicht nur "xxx". Ich bin mir der Gefahren internierter Saiten bewusst.
Lawrence Dol
1
Es gibt einen Unterschied zwischen "Synchronisieren auf dem Objekt selbst" und "Synchronisieren auf einer Referenz, die explizit zum Zweck der Synchronisierung verfügbar gemacht wurde". Wenn Sie mehrere Clients zum Synchronisieren benötigen, legen Sie eine Referenz für diese zur Verfügung. Das macht es viel offensichtlicher, was los ist. Immer noch keine Notwendigkeit, "dies" zu sperren.
Jon Skeet
2

Bei der Synchronisierung in Java werden häufig Vorgänge auf derselben Instanz synchronisiert. Die Synchronisierung ist thisdann sehr idiomatisch, da thises sich um eine gemeinsame Referenz handelt, die automatisch zwischen verschiedenen Instanzmethoden (oder Abschnitten von) in einer Klasse verfügbar ist.

Die Verwendung einer anderen Referenz speziell zum Sperren, beispielsweise durch Deklarieren und Initialisieren eines privaten Felds Object lock = new Object(), habe ich nie benötigt oder verwendet. Ich denke, es ist nur nützlich, wenn Sie eine externe Synchronisation für zwei oder mehr nicht synchronisierte Ressourcen innerhalb eines Objekts benötigen, obwohl ich immer versuchen würde, eine solche Situation in eine einfachere Form umzugestalten.

Auf jeden Fall wird implizit (synchronisierte Methode) oder explizit häufig synchronized(this)verwendet, auch in den Java-Bibliotheken. Es ist eine gute Redewendung und sollte, falls zutreffend, immer Ihre erste Wahl sein.

Eljenso
quelle
1

Was Sie synchronisieren, hängt davon ab, welche anderen Threads, die möglicherweise mit diesem Methodenaufruf in Konflikt geraten, synchronisiert werden können.

Wenn thises sich um ein Objekt handelt, das nur von einem Thread verwendet wird, und wir auf ein veränderbares Objekt zugreifen, das von Threads gemeinsam genutzt wird, ist es ein guter Kandidat, über dieses Objekt zu synchronisieren. Die Synchronisierung auf thishat keinen Sinn, da ein anderer Thread, der dieses gemeinsam genutzte Objekt ändert, möglicherweise nicht einmal weiß this, aber kennt das Objekt.

Andererseits ist eine Synchronisierung thissinnvoll, wenn viele Threads gleichzeitig Methoden dieses Objekts aufrufen, z. B. wenn wir uns in einem Singleton befinden.

Beachten Sie, dass eine synchronisierte Methode häufig nicht die beste Option ist, da wir die gesamte Zeit über eine Sperre halten. Wenn es zeitaufwändige, aber thread-sichere Teile und ein nicht so zeitaufwändiges thread-unsicheres Teil enthält, ist die Synchronisierung über die Methode sehr falsch.

Hans-Peter Störr
quelle
1

Fast alle Blöcke synchronisieren sich darauf, aber gibt es einen bestimmten Grund dafür? Gibt es noch andere Möglichkeiten?

Diese Deklaration synchronisiert die gesamte Methode.

private synchronized void doSomething() {

Diese Deklaration synchronisierte einen Teil des Codeblocks anstelle der gesamten Methode.

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

Von der Oracle - Dokumentation Seite

Das Synchronisieren dieser Methoden hat zwei Auswirkungen:

Erstens ist es nicht möglich, dass zwei Aufrufe synchronisierter Methoden für dasselbe Objekt verschachtelt werden . Wenn ein Thread eine synchronisierte Methode für ein Objekt ausführt, blockieren alle anderen Threads, die synchronisierte Methoden für denselben Objektblock aufrufen (Ausführung aussetzen), bis der erste Thread mit dem Objekt fertig ist.

Gibt es noch andere Möglichkeiten? Gibt es Best Practices für das zu synchronisierende Objekt? (wie private Instanzen von Object?)

Es gibt viele Möglichkeiten und Alternativen zur Synchronisation. Sie können Ihren Code-Thread sicher machen, indem Sie Parallelitäts- APIs auf hoher Ebene verwenden (verfügbar seit JDK 1.5).

Lock objects
Executors
Concurrent collections
Atomic variables
ThreadLocalRandom

Weitere Informationen finden Sie in den folgenden SE-Fragen:

Synchronisation gegen Sperre

Vermeiden Sie (dies) in Java synchronisiert?

Ravindra Babu
quelle
1

Die Best Practices bestehen darin, ein Objekt zu erstellen, das ausschließlich die Sperre bereitstellt:

private final Object lock = new Object();

private void doSomething() {
  // thread-safe code
  synchronized(lock) {
    // thread-unsafe code
  }
  // thread-safe code
}

Auf diese Weise können Sie sicher sein, dass kein aufrufender Code Ihre Methode jemals durch eine unbeabsichtigte synchronized(yourObject)Leitung blockieren kann .

( Dank an @jared und @ yuval-adam, die dies oben ausführlicher erklärt haben. )

Ich vermute, dass die Popularität der Verwendung thisin Tutorials von Sun javadoc stammt: https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

Epox
quelle