Synchronizacja pola innego niż końcowe


91

Ostrzeżenie jest wyświetlane za każdym razem, gdy przeprowadzam synchronizację w nieostatecznym polu klasy. Oto kod:

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  
     {  
     }  
   }  
 } 

więc zmieniłem kodowanie w następujący sposób:

 public class X  
 {  

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

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

Nie jestem pewien, czy powyższy kod jest właściwym sposobem synchronizacji w nieostatecznym polu klasy. Jak mogę zsynchronizować pole inne niż końcowe?

Odpowiedzi:


127

Przede wszystkim zachęcam do bardzo mocnego rozwiązywania problemów współbieżności na wyższym poziomie abstrakcji, czyli rozwiązywania ich przy użyciu klas z java.util.concurrent, takich jak ExecutorServices, Callables, Futures itp.

To powiedziawszy, nie ma nic złego w synchronizacji na polu niefinałowym jako takim. Musisz tylko pamiętać, że jeśli odniesienie do obiektu ulegnie zmianie, ta sama sekcja kodu może być uruchamiana równolegle . To znaczy, jeśli jeden wątek uruchamia kod w zsynchronizowanym bloku i ktoś wywołuje setO(...), inny wątek może jednocześnie uruchomić ten sam zsynchronizowany blok w tym samym wystąpieniu .

Zsynchronizuj się na obiekcie, do którego potrzebujesz wyłącznego dostępu (lub, jeszcze lepiej, na obiekcie przeznaczonym do jego ochrony).


1
Mówię, że jeśli synchronizujesz się na niekońcowym polu, powinieneś być świadomy faktu, że fragment kodu działa z wyłącznym dostępem do obiektu, do którego się oodwołuje w momencie osiągnięcia zsynchronizowanego bloku. Jeśli obiekt, który oodnosi się do zmian, może przyjść inny wątek i wykonać zsynchronizowany blok kodu.
aioobe

42
Nie zgadzam się z twoją praktyczną zasadą - wolę synchronizować obiekt, którego jedynym celem jest ochrona innego stanu. Jeśli nigdy nie robisz nic z obiektem innym niż blokowanie go, wiesz na pewno, że żaden inny kod nie może go zablokować. Jeśli zablokujesz „prawdziwy” obiekt, którego metody następnie wywołasz, ten obiekt może również zsynchronizować się ze sobą, co utrudnia rozumowanie co do blokowania.
Jon Skeet

9
Jak mówię w mojej odpowiedzi, myślę, że musiałbym bardzo dokładnie uzasadnić, dlaczego chcesz to zrobić. Nie polecałbym też synchronizacji this- zalecałbym utworzenie końcowej zmiennej w klasie wyłącznie do celów blokowania , które uniemożliwi innym blokowanie tego samego obiektu.
Jon Skeet

1
To kolejny dobry punkt i zgadzam się; blokowanie zmiennej nieostatecznej zdecydowanie wymaga starannego uzasadnienia.
aioobe

Nie jestem pewien co do problemów z widocznością pamięci związanych ze zmianą obiektu używanego do synchronizacji. Myślę, że możesz mieć duże problemy ze zmianą obiektu, a następnie polegając na tym, że kod zobaczy tę zmianę poprawnie, tak aby „ta sama sekcja kodu mogła być uruchamiana równolegle”. Nie jestem pewien, jakie gwarancje są rozszerzone przez model pamięci do widoczności pamięci pól, które są używane do blokowania, w przeciwieństwie do zmiennych dostępnych w bloku synchronizacji. Moja praktyczna zasada brzmi: jeśli zsynchronizujesz coś, powinno to być ostateczne.
Mike Q

47

To naprawdę nie jest dobry pomysł - ponieważ zsynchronizowane bloki nie są już tak naprawdę synchronizowane w spójny sposób.

Zakładając, że zsynchronizowane bloki mają zapewniać, że tylko jeden wątek ma dostęp do niektórych udostępnionych danych naraz, rozważ:

  • Wątek 1 wchodzi do zsynchronizowanego bloku. Yay - ma wyłączny dostęp do udostępnionych danych ...
  • Wywołania wątku 2 setO ()
  • Wątek 3 (lub nadal 2 ...) wchodzi do zsynchronizowanego bloku. Eek! Uważa, że ​​ma wyłączny dostęp do współdzielonych danych, ale wątek 1 nadal się nim zajmuje ...

Dlaczego chcesz , żeby to się stało? Może są jakieś bardzo wyspecjalizowane sytuacje, w których ma to sens ... ale musiałbyś mi przedstawić konkretny przypadek użycia (wraz ze sposobami złagodzenia tego rodzaju scenariusza, który podałem powyżej), zanim byłbym zadowolony z to.


2
@aioobe: Ale wtedy wątek 1 może nadal uruchamiać kod, który mutuje listę (i często się do niego odwołuje o) - i w trakcie jego wykonywania rozpocząć mutowanie innej listy. Jaki byłby to dobry pomysł? Myślę, że zasadniczo nie zgadzamy się co do tego, czy warto blokować obiekty, których dotykasz w inny sposób, czy nie. Wolałbym raczej móc rozumować na temat mojego kodu bez żadnej wiedzy o tym, co robi inny kod w zakresie blokowania.
Jon Skeet

2
@Felype: Wygląda na to, że powinieneś zadać bardziej szczegółowe pytanie jako osobne pytanie - ale tak, często tworzyłem oddzielne obiekty, tak jak zamki.
Jon Skeet

3
@VitBernatik: Nie. Jeśli wątek X zacznie modyfikować konfigurację, wątek Y zmieni wartość synchronizowanej zmiennej, a następnie wątek Z zacznie modyfikować konfigurację, wtedy X i Z będą modyfikować konfigurację w tym samym czasie, co jest złe .
Jon Skeet

1
Krótko mówiąc, bezpieczniej jest, jeśli zawsze deklarujemy takie obiekty zamka jako ostateczne, prawda?
St.Antario

2
@LinkTheProgrammer: „Zsynchronizowana metoda synchronizuje każdy obiekt w instancji” - nie, nie. To po prostu nieprawda i powinieneś powrócić do zrozumienia synchronizacji.
Jon Skeet

12

Zgadzam się z jednym z komentarzem Jana: Należy zawsze używać ostatecznej blokady manekina podczas dostępu nieostatecznej zmienną, aby zapobiec niespójności w przypadku zmian referencyjnych zmiennej. Tak więc w każdym przypadku i jako pierwsza zasada:

Zasada nr 1: Jeśli pole nie jest ostateczne, zawsze używaj (prywatnego) atrapy ostatecznej blokady.

Powód 1: Trzymasz blokadę i samodzielnie zmieniasz odniesienie do zmiennej. Kolejny wątek oczekujący na zewnątrz zsynchronizowanego zamka będzie mógł wejść do strzeżonego bloku.

Powód # 2: Trzymasz blokadę, a inny wątek zmienia odniesienie do zmiennej. Wynik jest taki sam: inny wątek może wejść do strzeżonego bloku.

Ale kiedy używasz ostatecznej atrapy blokady, pojawia się inny problem : możesz otrzymać nieprawidłowe dane, ponieważ twój nie ostateczny obiekt zostanie zsynchronizowany z pamięcią RAM tylko podczas wywoływania synchronizacji (obiekt). A więc jako druga praktyczna zasada:

Zasada nr 2: Podczas blokowania obiektu, który nie jest ostateczny, zawsze musisz zrobić jedno i drugie: Użyć atrapy ostatecznej blokady i blokady obiektu innego niż ostateczny w celu synchronizacji pamięci RAM. (Jedyną alternatywą będzie zadeklarowanie wszystkich pól obiektu jako niestabilnych!)

Te blokady są również nazywane „zamkami zagnieżdżonymi”. Pamiętaj, że musisz dzwonić do nich zawsze w tej samej kolejności, w przeciwnym razie otrzymasz martwy zamek :

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

Jak widać, piszę dwa zamki bezpośrednio w tej samej linii, ponieważ zawsze należą do siebie. W ten sposób możesz nawet wykonać 10 blokad zagnieżdżania:

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

Zauważ, że ten kod nie zepsuje się, jeśli po prostu zdobędziesz wewnętrzny zamek, tak jak synchronized (LOCK3)w przypadku innych wątków. Ale zepsuje się, jeśli wywołasz inny wątek, coś takiego:

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

Istnieje tylko jedno obejście takich zagnieżdżonych blokad podczas obsługi pól innych niż końcowe:

Zasada # 2 - Alternatywa: Zadeklaruj wszystkie pola obiektu jako niestabilne. (Nie będę tu mówić o wadach robienia tego, np. Zapobieganie przechowywaniu w pamięci podręcznej na poziomie x, nawet dla odczytów, aso.)

Dlatego aioobe ma rację: po prostu użyj java.util.concurrent. Lub zacznij rozumieć wszystko o synchronizacji i zrób to sam z zagnieżdżonymi zamkami. ;)

Aby uzyskać więcej informacji, dlaczego synchronizacja w polach innych niż ostateczne się psuje, zajrzyj do mojego przypadku testowego: https://stackoverflow.com/a/21460055/2012947

Aby uzyskać więcej informacji, dlaczego w ogóle potrzebujesz synchronizacji z powodu pamięci RAM i pamięci podręcznych, zajrzyj tutaj: https://stackoverflow.com/a/21409975/2012947


1
Myślę, że musisz zawinąć setter oz synchronizowanym (LOCK), aby ustanowić relację „zdarza się przed” między ustawieniem a obiektem odczytu o.
Omawiam

Używam dataObject do synchronizowania dostępu do członków dataObject. Jak to źle? Jeśli dataObject zacznie wskazywać inne miejsce, chcę, aby był zsynchronizowany z nowymi danymi, aby zapobiec ich modyfikowaniu przez współbieżne wątki. Jakieś problemy z tym?
Harmen

2

Tak naprawdę nie widzę tutaj poprawnej odpowiedzi, to znaczy, jest to całkowicie w porządku.

Nie jestem nawet pewien, dlaczego to ostrzeżenie, nie ma w tym nic złego. JVM upewnia się, że podczas odczytywania wartości otrzymujesz z powrotem jakiś prawidłowy obiekt (lub wartość null) i możesz synchronizować dowolny obiekt.

Jeśli planujesz faktycznie zmienić blokadę, gdy jest ona używana (w przeciwieństwie do np. Zmiany jej z metody init, zanim zaczniesz jej używać), musisz wprowadzić zmienną, którą planujesz zmienić volatile. Następnie wszystko, co musisz zrobić, to zsynchronizować zarówno stary, jak i nowy obiekt, i możesz bezpiecznie zmienić wartość

public volatile Object lock;

...

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

Tam. To nie jest skomplikowane, pisanie kodu z blokadami (muteksami) jest całkiem proste. Pisanie kodu bez nich (kod bez blokady) jest trudne.


To może się nie udać. Powiedzmy o rozpoczęło się jako odniesienie do O1, następnie gwint T1 blokuje się o (= O1) i O2 i ustawia o na O2. W tym samym czasie Thread T2 blokuje O1 i czeka na odblokowanie go przez T1. Kiedy otrzyma blokadę O1, ustawi o na O3. W tym scenariuszu, między T1 zwolnieniem O1 a T2 blokowaniem O1, O1 stał się nieważny do blokowania przez o. W tym momencie inny wątek może używać o (= O2) do blokowania i kontynuować nieprzerwany wyścig z T2.
GPS

2

EDYCJA: Więc to rozwiązanie (zgodnie z sugestią Jona Skeeta) może mieć problem z atomowością implementacji „synchronized (object) {}” podczas zmiany odniesienia do obiektu. Pytałem osobno i według pana Ericksona nie jest to wątkowo bezpieczne - patrz: Czy wejście w synchronizowany blok jest atomowe? . Więc weź to jako przykład, jak tego nie robić - z linkami dlaczego;)

Zobacz kod, jak by działał, gdyby synchronized () był atomowy:

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();
    }

}

1
To może być w porządku - ale nie wiem, czy w modelu pamięci jest jakaś gwarancja, że ​​wartość, na której synchronizujesz, jest ostatnią zapisaną - nie sądzę, aby istniała gwarancja atomowego „odczytu i synchronizacji”. Osobiście dla uproszczenia staram się unikać synchronizacji na monitorach, które i tak mają inne zastosowania. (Przez posiadanie oddzielnego pola, kod staje się wyraźnie poprawne zamiast rozumu o tym ostrożnie.)
Jon Skeet

@Jon. Dzięki za odpowiedź! Słyszę twoje zmartwienie. Zgadzam się na ten przypadek, gdy zewnętrzny zamek pozwoliłby uniknąć kwestii „zsynchronizowanej atomowości”. Tak więc byłoby lepiej. Chociaż mogą istnieć przypadki, w których chcesz wprowadzić w czasie wykonywania więcej konfiguracji i udostępnić inną konfigurację dla różnych grup wątków (chociaż nie jest to mój przypadek). I wtedy to rozwiązanie może stać się interesujące. Zamieściłem pytanie stackoverflow.com/questions/29217266/... zsynchronizowanego () atomowości - więc zobaczymy, czy może on być stosowany (i jest ktoś odpowiedź)
Vit Bernatik

2

AtomicReference odpowiada Twoim wymaganiom.

Z dokumentacji java o pakiecie atomic :

Mały zestaw narzędzi zawierający klasy, które obsługują programowanie z zabezpieczeniem wątkowym bez blokowania na pojedynczych zmiennych. Zasadniczo klasy w tym pakiecie rozszerzają pojęcie wartości ulotnych, pól i elementów tablic na te, które również zapewniają atomową warunkową operację aktualizacji w postaci:

boolean compareAndSet(expectedValue, updateValue);

Przykładowy kod:

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

W powyższym przykładzie zamieniasz Stringna własnyObject

Powiązane pytanie SE:

Kiedy używać AtomicReference w Javie?


1

Jeśli onigdy się nie zmienia przez cały okres istnienia instancjiX , druga wersja ma lepszy styl, niezależnie od tego, czy wymagana jest synchronizacja.

Nie da się odpowiedzieć na pytanie, czy coś jest nie tak z pierwszą wersją, nie wiedząc, co jeszcze dzieje się na tych zajęciach. Zgodziłbym się z kompilatorem, że wygląda na podatny na błędy (nie powtórzę tego, co powiedzieli inni).


1

Po prostu dodając moje dwa centy: otrzymałem to ostrzeżenie, gdy użyłem komponentu, który jest tworzony przez projektanta, więc jego pole nie może być tak naprawdę ostateczne, ponieważ konstruktor nie może przyjmować parametrów. Innymi słowy, miałem quasi-końcowe pole bez końcowego słowa kluczowego.

Myślę, że dlatego jest to tylko ostrzeżenie: prawdopodobnie robisz coś złego, ale może też być słuszne.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.