Synchronisation des nicht endgültigen Feldes

88

Jedes Mal, wenn ich in einem nicht endgültigen Klassenfeld synchronisiere, wird eine Warnung angezeigt. Hier ist der Code:

public class X  
{  
   private Object o;  

   public void setO(Object o)  
   {  
     this.o = o;  
   }  

   public void x()  
   {  
     synchronized (o) // synchronization on a non-final field  
     {  
     }  
   }  
 } 

Deshalb habe ich die Codierung folgendermaßen geändert:

 public class X  
 {  

   private final Object o;       
   public X()
   {  
     o = new Object();  
   }  

   public void x()  
   {  
     synchronized (o)
     {  
     }  
   }  
 }  

Ich bin nicht sicher, ob der obige Code der richtige Weg ist, um in einem nicht endgültigen Klassenfeld zu synchronisieren. Wie kann ich ein nicht endgültiges Feld synchronisieren?

divz
quelle

Antworten:

126

Zunächst möchte ich Sie ermutigen, sich wirklich zu bemühen, Parallelitätsprobleme auf einer höheren Abstraktionsebene zu lösen, dh sie mithilfe von Klassen aus java.util.concurrent wie ExecutorServices, Callables, Futures usw. zu lösen.

Davon abgesehen ist nichts falsch daran, auf einem nicht endgültigen Feld per se zu synchronisieren. Sie müssen nur berücksichtigen, dass bei einer Änderung der Objektreferenz möglicherweise derselbe Codeabschnitt parallel ausgeführt wird . Das heißt, wenn ein Thread den Code im synchronisierten Block ausführt und jemand aufruft setO(...), kann ein anderer Thread denselben synchronisierten Block gleichzeitig auf derselben Instanz ausführen.

Synchronisieren Sie das Objekt, auf das Sie exklusiven Zugriff benötigen (oder besser noch ein Objekt, das für die Bewachung vorgesehen ist).

aioobe
quelle
1
Ich sage, wenn Sie in einem nicht endgültigen Feld synchronisieren, sollten Sie sich der Tatsache bewusst sein, dass das Codeausschnitt mit exklusivem Zugriff auf das Objekt ausgeführt owird, auf das zum Zeitpunkt des Erreichens des synchronisierten Blocks verwiesen wurde. Wenn sich das Objekt, das osich auf Änderungen bezieht, ändert, kann ein anderer Thread den synchronisierten Codeblock ausführen.
Aioobe
42
Ich bin mit Ihrer Faustregel nicht einverstanden. Ich bevorzuge die Synchronisierung mit einem Objekt, dessen einziger Zweck darin besteht, einen anderen Zustand zu schützen . Wenn Sie niemals etwas anderes mit einem Objekt tun, als es zu sperren, wissen Sie sicher, dass kein anderer Code es sperren kann. Wenn Sie ein "echtes" Objekt sperren, dessen Methoden Sie dann aufrufen, kann dieses Objekt auch mit sich selbst synchronisieren, was es schwieriger macht, über das Sperren nachzudenken.
Jon Skeet
9
Wie ich in meiner Antwort sage, denke ich, ich müsste es mir sehr sorgfältig begründen lassen, warum Sie so etwas tun möchten . Und ich würde auch nicht empfehlen, auf zu synchronisieren this- ich würde empfehlen, eine endgültige Variable in der Klasse nur zum Zwecke des Sperren zu erstellen , wodurch verhindert wird, dass andere Personen dasselbe Objekt sperren.
Jon Skeet
1
Das ist ein weiterer guter Punkt, und ich stimme zu; Das Sperren einer nicht endgültigen Variablen erfordert auf jeden Fall eine sorgfältige Begründung.
Aioobe
Ich bin mir nicht sicher, ob beim Speichern eines Objekts, das für die Synchronisierung verwendet wird, Probleme mit der Speichersichtbarkeit auftreten. Ich denke, Sie würden große Probleme bekommen, ein Objekt zu ändern und sich dann darauf zu verlassen, dass der Code diese Änderung korrekt sieht, so dass "derselbe Codeabschnitt parallel ausgeführt werden kann". Ich bin nicht sicher, welche Garantien das Speichermodell gegebenenfalls auf die Speichersichtbarkeit von Feldern erweitert, die zum Sperren verwendet werden, im Gegensatz zu den Variablen, auf die innerhalb des Synchronisierungsblocks zugegriffen wird. Meine Faustregel wäre, wenn Sie auf etwas synchronisieren, sollte es endgültig sein.
Mike Q
46

Das ist wirklich keine gute Idee, da Ihre synchronisierten Blöcke nicht mehr wirklich konsistent synchronisiert sind .

Angenommen, die synchronisierten Blöcke sollen sicherstellen, dass jeweils nur ein Thread auf gemeinsam genutzte Daten zugreift. Beachten Sie Folgendes:

  • Thread 1 tritt in den synchronisierten Block ein. Ja - es hat exklusiven Zugriff auf die freigegebenen Daten ...
  • Thread 2 ruft setO () auf
  • Thread 3 (oder noch 2 ...) tritt in den synchronisierten Block ein. Eek! Es glaubt, dass es exklusiven Zugriff auf die gemeinsam genutzten Daten hat, aber Thread 1 läuft immer noch damit ...

Warum würden Sie wollen dies geschehen? Vielleicht gibt es einige sehr spezielle Situationen, in denen es sinnvoll ist ... aber Sie müssten mir einen bestimmten Anwendungsfall (zusammen mit Möglichkeiten zur Abschwächung des oben angegebenen Szenarios) vorstellen, bevor ich zufrieden bin es.

Jon Skeet
quelle
2
@aioobe: Aber dann könnte Thread 1 immer noch Code ausführen, der die Liste mutiert (und häufig darauf verweist o) - und während der Ausführung beginnt eine andere Liste zu mutieren. Wie wäre das eine gute Idee? Ich denke, wir sind uns grundsätzlich nicht einig darüber, ob es eine gute Idee ist, Objekte zu fixieren, die Sie auf andere Weise berühren. Ich wäre lieber in der Lage, über meinen Code nachzudenken, ohne zu wissen, was anderer Code in Bezug auf das Sperren tut.
Jon Skeet
2
@Felype: Es hört sich so an, als ob Sie eine detailliertere Frage als separate Frage stellen sollten - aber ja, ich würde oft separate Objekte nur als Sperren erstellen.
Jon Skeet
3
@VitBernatik: Nein. Wenn Thread X beginnt, die Konfiguration zu ändern, ändert Thread Y den Wert der zu synchronisierenden Variablen, dann beginnt Thread Z, die Konfiguration zu ändern, und sowohl X als auch Z ändern gleichzeitig die Konfiguration, was schlecht ist .
Jon Skeet
1
Kurz gesagt, es ist sicherer, wenn wir solche Sperrobjekte immer für endgültig erklären, richtig?
St.Antario
2
@LinkTheProgrammer: "Eine synchronisierte Methode synchronisiert jedes einzelne Objekt in der Instanz" - nein, das tut es nicht. Das ist einfach nicht wahr, und Sie sollten Ihr Verständnis der Synchronisation überdenken.
Jon Skeet
12

Ich stimme einem Kommentar von John zu: Sie müssen beim Zugriff auf eine nicht endgültige Variable immer einen Dummy für die endgültige Sperre verwenden, um Inkonsistenzen bei Referenzänderungen der Variablen zu vermeiden. Also auf jeden Fall und als erste Faustregel:

Regel 1: Wenn ein Feld nicht endgültig ist, verwenden Sie immer einen (privaten) Dummy für die endgültige Sperre.

Grund Nr. 1: Sie halten die Sperre und ändern die Referenz der Variablen selbst. Ein anderer Thread, der außerhalb der synchronisierten Sperre wartet, kann in den geschützten Block eintreten.

Grund Nr. 2: Sie halten die Sperre und ein anderer Thread ändert die Referenz der Variablen. Das Ergebnis ist das gleiche: Ein anderer Thread kann in den geschützten Block eintreten.

Bei Verwendung eines Dummy für die endgültige Sperre gibt es jedoch ein weiteres Problem : Möglicherweise werden falsche Daten angezeigt, da Ihr nicht endgültiges Objekt nur beim Aufruf von synchronize (Objekt) mit dem RAM synchronisiert wird. Als zweite Faustregel gilt:

Regel 2: Wenn Sie ein nicht endgültiges Objekt sperren, müssen Sie immer beides tun: Verwenden eines endgültigen Sperr-Dummys und der Sperre des nicht endgültigen Objekts für die RAM-Synchronisation. (Die einzige Alternative besteht darin, alle Felder des Objekts als flüchtig zu deklarieren!)

Diese Sperren werden auch als "verschachtelte Sperren" bezeichnet. Beachten Sie, dass Sie sie immer in derselben Reihenfolge aufrufen müssen, da Sie sonst ein Deadlock erhalten :

public class X {
    private final LOCK;
    private Object o;

    public void setO(Object o){
        this.o = o;  
    }  

    public void x() {
        synchronized (LOCK) {
        synchronized(o){
            //do something with o...
        }
        }  
    }  
} 

Wie Sie sehen, schreibe ich die beiden Schlösser direkt in dieselbe Zeile, weil sie immer zusammen gehören. Auf diese Weise können Sie sogar 10 Verschachtelungssperren erstellen:

synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
    //entering the locked space
}
}
}
}

Beachten Sie, dass dieser Code nicht beschädigt wird, wenn Sie nur eine innere Sperre wie synchronized (LOCK3)bei einem anderen Thread erwerben . Aber es wird kaputt gehen, wenn Sie einen anderen Thread wie diesen aufrufen:

synchronized (LOCK4) {
synchronized (LOCK1) {  //dead lock!
synchronized (LOCK3) {
synchronized (LOCK2) {
    //will never enter here...
}
}
}
}

Es gibt nur eine Problemumgehung für solche verschachtelten Sperren beim Behandeln nicht endgültiger Felder:

Regel 2 - Alternative: Deklarieren Sie alle Felder des Objekts als flüchtig. (Ich werde hier nicht über die Nachteile sprechen, z. B. das Verhindern von Speicher in X-Level-Caches, auch für Lesevorgänge.)

Aioobe ist also ganz richtig: Verwenden Sie einfach java.util.concurrent. Oder beginnen Sie, alles über die Synchronisierung zu verstehen, und tun Sie dies selbst mit verschachtelten Sperren. ;)

Weitere Informationen dazu, warum die Synchronisierung bei nicht endgültigen Feldern unterbrochen wird, finden Sie in meinem Testfall: https://stackoverflow.com/a/21460055/2012947

Weitere Informationen dazu, warum Sie aufgrund von RAM und Caches überhaupt synchronisiert werden müssen, finden Sie hier: https://stackoverflow.com/a/21409975/2012947

Marcus
quelle
1
Ich denke, dass Sie den Setter omit einem synchronisierten (LOCK) umschließen müssen, um eine "Vorher-Geschehen" -Beziehung zwischen dem Setzen und dem Lesen des Objekts herzustellen o. Ich diskutiere dies in einer ähnlichen Frage von mir: stackoverflow.com/questions/32852464/…
Petrakeas
Ich verwende dataObject, um den Zugriff auf dataObject-Mitglieder zu synchronisieren. Wie ist das falsch? Wenn das dataObject auf eine andere Stelle zeigt, möchte ich, dass es mit den neuen Daten synchronisiert wird, um zu verhindern, dass gleichzeitige Threads sie ändern. Probleme damit?
Harmen
2

Ich sehe hier nicht wirklich die richtige Antwort, das heißt, es ist vollkommen in Ordnung, dies zu tun.

Ich bin mir nicht mal sicher, warum es eine Warnung ist, daran ist nichts auszusetzen. Die JVM stellt sicher , dass Sie erhalten ein gültiges Objekt zurück (oder null) , wenn Sie einen Wert lesen, und Sie können auf synchronisieren jedes Objekt.

Wenn Sie vorhaben, die Sperre während der Verwendung tatsächlich zu ändern (anstatt sie beispielsweise von einer Init-Methode zu ändern, bevor Sie sie verwenden), müssen Sie die Variable festlegen, die Sie ändern möchten volatile. Dann alles , was Sie tun müssen, ist auf die Synchronisierung sowohl dem alten und dem neuen Objekt, und Sie können sicher den Wert ändern

public volatile Object lock;

...

synchronized (lock) {
    synchronized (newObject) {
        lock = newObject;
    }
}

Dort. Es ist nicht kompliziert, das Schreiben von Code mit Sperren (Mutexen) ist eigentlich recht einfach. Das Schreiben von Code ohne sie (sperrfreier Code) ist schwierig.

Evan Dark
quelle
Dies funktioniert möglicherweise nicht. Angenommen, o wurde als Referenz für O1 gestartet, dann sperrt Thread T1 o (= O1) und O2 und setzt o auf O2. Gleichzeitig sperrt Thread T2 O1 und wartet darauf, dass T1 es entsperrt. Wenn es die Sperre O1 empfängt, setzt es o auf O3. In diesem Szenario wurde O1 zwischen dem Freigeben von O1 durch T1 und dem Sperren von O1 durch T2 für das Sperren durch o ungültig. Zu diesem Zeitpunkt kann ein anderer Thread o (= O2) zum Sperren verwenden und im Rennen mit T2 ununterbrochen fortfahren.
GPS
2

EDIT: Diese Lösung (wie von Jon Skeet vorgeschlagen) hat möglicherweise ein Problem mit der Atomizität der Implementierung von "synchronized (object) {}", während sich die Objektreferenz ändert. Ich habe separat gefragt und laut Mr. Erickson ist es nicht threadsicher - siehe: Ist die Eingabe eines synchronisierten Blocks atomar? . Nehmen Sie es als Beispiel, wie man es NICHT macht - mit Links warum;)

Sehen Sie sich den Code an, wie es funktionieren würde, wenn synchronized () atomar wäre:

public class Main {
    static class Config{
        char a='0';
        char b='0';
        public void log(){
            synchronized(this){
                System.out.println(""+a+","+b);
            }
        }
    }

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

        Doer(char id) {
            this.id = id;
        }

        public void mySleep(long ms){
            try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();}
        }

        public void run() {
            System.out.println("Doer "+id+" beg");
            if(id == 'X'){
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(1000);
                    // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend
                    // here it would be modifying different cfg (cos Y will change it).
                    // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object
                    cfg.b=id;
                }
            }
            if(id == 'Y'){
                mySleep(333);
                synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok
                {
                    cfg = new Config();  // introduce new configuration
                    // be aware - don't expect here to be synchronized on new cfg!
                    // Z might already get a lock
                }
            }
            if(id == 'Z'){
                mySleep(666);
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(100);
                    cfg.b=id;
                }
            }
            System.out.println("Doer "+id+" end");
            cfg.log();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Doer X = new Doer('X');
        Doer Y = new Doer('Y');
        Doer Z = new Doer('Z');
        X.start();
        Y.start();
        Z.start();
    }

}
Vit Bernatik
quelle
1
Dies mag in Ordnung sein - aber ich weiß nicht, ob es im Speichermodell eine Garantie dafür gibt, dass der Wert, auf dem Sie synchronisieren, der zuletzt geschriebene ist - ich glaube, es gibt keine Garantie für atomares "Lesen und Synchronisieren". Persönlich versuche ich der Einfachheit halber zu vermeiden, auf Monitoren zu synchronisieren, die ohnehin andere Verwendungszwecke haben. (Durch ein separates Feld wird der Code eindeutig korrekt, anstatt sorgfältig darüber nachdenken zu müssen.)
Jon Skeet
@ Jon. Danke für die Antwort! Ich höre deine Sorge. Ich stimme für diesen Fall zu, dass die externe Sperre die Frage der "synchronisierten Atomizität" vermeiden würde. Daher wäre vorzuziehen. Obwohl es Fälle geben kann, in denen Sie zur Laufzeit mehr Konfiguration einführen und unterschiedliche Konfigurationen für verschiedene Thread-Gruppen freigeben möchten (obwohl dies nicht mein Fall ist). Und dann könnte diese Lösung interessant werden. Ich habe die Frage stackoverflow.com/questions/29217266/… zur synchronisierten () Atomizität gepostet - also werden wir sehen, ob sie verwendet werden kann (und jemand antwortet)
Vit Bernatik
2

AtomicReference passt zu Ihrer Anforderung.

Von Java - Dokumentation über Atom- Paket:

Ein kleines Toolkit mit Klassen, die eine sperrenfreie threadsichere Programmierung für einzelne Variablen unterstützen. Im Wesentlichen erweitern die Klassen in diesem Paket den Begriff der flüchtigen Werte, Felder und Array-Elemente auf diejenigen, die auch eine atomare bedingte Aktualisierungsoperation des Formulars bereitstellen:

boolean compareAndSet(expectedValue, updateValue);

Beispielcode:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

Im obigen Beispiel ersetzen Sie durch StringIhr eigenesObject

Verwandte SE-Frage:

Wann wird AtomicReference in Java verwendet?

Ravindra Babu
quelle
1

Wenn osich während der Lebensdauer einer Instanz von nie ändert X, ist die zweite Version besser, unabhängig davon, ob es sich um eine Synchronisierung handelt.

Ob mit der ersten Version etwas nicht stimmt, kann nicht beantwortet werden, ohne zu wissen, was in dieser Klasse noch vor sich geht. Ich würde dem Compiler eher zustimmen, dass er fehleranfällig aussieht (ich werde nicht wiederholen, was die anderen gesagt haben).

NPE
quelle
1

Ich füge nur meine zwei Cent hinzu: Ich hatte diese Warnung, als ich eine Komponente verwendete, die durch den Designer instanziiert wurde, sodass das Feld nicht wirklich endgültig sein kann, da der Konstruktor keine Parameter annehmen kann. Mit anderen Worten, ich hatte ein quasi endgültiges Feld ohne das endgültige Schlüsselwort.

Ich denke, deshalb ist es nur eine Warnung: Sie machen wahrscheinlich etwas falsch, aber es könnte auch richtig sein.

Peenut
quelle