Unikać synchronizacji (tej) w Javie?

381

Za każdym razem, gdy pojawia się pytanie na temat SO dotyczące synchronizacji Java, niektórzy bardzo chętnie wskazują, że synchronized(this)należy tego unikać. Zamiast tego twierdzą, że preferowana jest blokada prywatnego odwołania.

Niektóre z podanych powodów to:

Inni ludzie, w tym ja, twierdzą, że synchronized(this)jest to często używany idiom (także w bibliotekach Java), bezpieczny i dobrze rozumiany. Nie należy tego unikać, ponieważ masz błąd i nie masz pojęcia, co dzieje się w twoim programie wielowątkowym. Innymi słowy: jeśli ma to zastosowanie, użyj go.

Chciałbym zobaczyć przykłady z prawdziwego świata (bez fobarów), w których thislepiej unikać unikania blokady, gdybym synchronized(this)również wykonał zadanie.

Dlatego: czy zawsze należy go unikać synchronized(this)i zastępować zamkiem prywatnego odniesienia?


Kilka dodatkowych informacji (zaktualizowanych po udzieleniu odpowiedzi):

  • mówimy o synchronizacji instancji
  • brane są pod uwagę zarówno domniemane ( synchronizedmetody), jak i jawna formasynchronized(this)
  • jeśli cytujesz Blocha lub inne autorytety na ten temat, nie pomijaj części, które ci się nie podobają (np. Efektywna Java, element dotyczący bezpieczeństwa wątków: Zazwyczaj jest to blokada samej instancji, ale są wyjątki).
  • jeśli potrzebujesz szczegółowości w blokowaniu innym niż synchronized(this)zapewnia, synchronized(this)to nie dotyczy, więc nie o to chodzi
eljenso
źródło
4
Chciałbym również zauważyć, że kontekst jest ważny - bit „Zazwyczaj jest to blokada samej instancji” znajduje się w części dotyczącej dokumentowania klasy warunkowo bezpiecznej dla wątków, gdy podajesz blokadę do wiadomości publicznej. Innymi słowy, zdanie to obowiązuje, gdy już podjąłeś decyzję.
Jon Skeet
W przypadku braku wewnętrznej synchronizacji i gdy potrzebna jest zewnętrzna synchronizacja, blokada jest często samą instancją, Bloch zasadniczo mówi. Dlaczego więc nie miałoby tak być w przypadku wewnętrznej synchronizacji z blokadą „tego”? (Ważność dokumentacji to kolejna kwestia.)
eljenso
Istnieje kompromis między rozszerzoną ziarnistością a dodatkowym obciążeniem pamięci podręcznej procesora i obciążeniami magistrali, ponieważ blokowanie na Zewnętrznym obiekcie najprawdopodobniej będzie wymagało oddzielnej linii pamięci podręcznej do modyfikacji i wymiany między pamięciami podręcznymi procesora (por. MESIF i MOESI).
ArtemGr
1
Myślę, że w świecie programowania obronnego można uniknąć błędów nie przez idiom, ale przez kod. Gdy ktoś zadaje mi pytanie „Jak zoptymalizowana jest twoja synchronizacja?”, Chcę powiedzieć „Bardzo” zamiast „Bardzo, chyba że ktoś inny nie zastosuje się do tego idiomu”.
Sgene9,

Odpowiedzi:

132

Omówię każdy punkt osobno.

  1. Niektóre złe kody mogą ukraść twoją blokadę (ta bardzo popularna, ma również wariant „przypadkowo”)

    Bardziej martwię się o przypadek . Sprowadza się to do tego, że takie użycie thisjest częścią ujawnionego interfejsu twojej klasy i powinno być udokumentowane. Czasami pożądana jest zdolność innego kodu do korzystania z zamka. Dotyczy to rzeczy takich jak Collections.synchronizedMap(patrz javadoc).

  2. Wszystkie zsynchronizowane metody w tej samej klasie używają dokładnie tej samej blokady, co zmniejsza przepustowość

    To zbyt uproszczone myślenie; samo pozbycie synchronized(this)się nie rozwiąże problemu. Właściwa synchronizacja przepustowości wymaga więcej przemyślenia.

  3. (Niepotrzebnie) ujawniasz za dużo informacji

    To jest wariant nr 1. Użycie synchronized(this)jest częścią twojego interfejsu. Jeśli nie chcesz tego ujawnić, nie rób tego.

Darron
źródło
1. „zsynchronizowany” nie jest częścią widocznego interfejsu twojej klasy. 2. zgadzają się 3. patrz 1.
eljenso
66
Zasadniczo zsynchronizowane (to) jest ujawnione, ponieważ oznacza to, że kod zewnętrzny może wpływać na działanie twojej klasy. Twierdzę więc, że musisz udokumentować go jako interfejs, nawet jeśli język tego nie robi.
Darron
15
Podobny. Zobacz Javadoc for Collections.synchronizedMap () - zwrócony obiekt używa zsynchronizowanego (this) wewnętrznie i oczekuje, że konsument skorzysta z tego, że użyje tej samej blokady do operacji atomowych na dużą skalę, takich jak iteracja.
Darron
3
W rzeczywistości Collections.synchronizedMap () NIE używa wewnętrznie zsynchronizowanego (this), używa prywatnego ostatecznego obiektu blokady.
Bas Leijdekkers
1
@Bas Leijdekkers: dokumentacja jasno określa, że ​​synchronizacja odbywa się na zwróconej instancji mapy. Co ciekawe, widoki zwracane przez keySet()i values()nie blokują się (ich) this, ale instancja mapy, co jest ważne, aby uzyskać spójne zachowanie dla wszystkich operacji na mapie. Przyczyną tego, że obiekt blokady jest rozłożony na zmienną, jest to, że podklasa SynchronizedSortedMappotrzebuje go do implementacji podmap blokujących oryginalne wystąpienie mapy.
Holger,
86

Po pierwsze, należy zauważyć, że:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

jest semantycznie równoważny z:

public synchronized void blah() {
  // do stuff
}

który jest jednym z powodów, aby nie używać synchronized(this). Możesz argumentować, że możesz robić rzeczy wokół synchronized(this)bloku. Zwykle powodem jest unikanie w ogóle sprawdzania zsynchronizowanego, co prowadzi do różnego rodzaju problemów z współbieżnością, w szczególności problemu podwójnego sprawdzania blokowania , który pokazuje, jak trudne może być wykonanie stosunkowo prostej kontroli Threadsafe.

Prywatny zamek to mechanizm obronny, który nigdy nie jest złym pomysłem.

Ponadto, jak wspomniałeś, prywatne zamki mogą kontrolować ziarnistość. Jeden zestaw operacji na obiekcie może być całkowicie niezwiązany z innym, ale synchronized(this)wzajemnie wyklucza dostęp do wszystkich z nich.

synchronized(this) po prostu nic ci nie daje.

Cletus
źródło
4
„zsynchronizowane (to) po prostu tak naprawdę nic ci nie daje”. Ok, zastępuję go synchronizacją (myPrivateFinalLock). Co mi to daje? Mówisz o tym, że jest to mechanizm obronny. Przed czym jestem chroniony?
eljenso
14
Jesteś chroniony przed przypadkowym (lub złośliwym) zablokowaniem „tego” przez obiekty zewnętrzne.
cletus
14
W ogóle nie zgadzam się z tą odpowiedzią: blokada powinna zawsze być utrzymywana przez możliwie najkrótszy czas, i właśnie dlatego powinieneś chcieć „robić rzeczy” wokół zsynchronizowanego bloku zamiast synchronizować całą metodę .
Olivier
5
Robienie rzeczy poza synchronizowanym blokiem zawsze ma dobre intencje. Chodzi o to, że ludzie często się mylą i nawet nie zdają sobie z tego sprawy, tak jak w przypadku problemu z podwójną kontrolą. Droga do piekła jest wybrukowana dobrymi chęciami.
cletus
16
Ogólnie nie zgadzam się z „X jest mechanizmem obronnym, co nigdy nie jest złym pomysłem”. Istnieje wiele niepotrzebnie rozdętego kodu z powodu takiego podejścia.
finnw
54

Podczas korzystania z synchronizacji (this) używasz instancji klasy jako samej blokady. Oznacza to, że gdy blokada jest uzyskiwana przez wątek 1 , wątek 2 powinien poczekać.

Załóżmy następujący kod:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

Metoda 1 modyfikująca zmienną a i metoda 2 modyfikująca zmienną b , należy unikać jednoczesnej modyfikacji tej samej zmiennej dwoma wątkami i tak jest. ALE podczas wątku modyfikującego a i wątku modyfikującego b niej można wykonać bez żadnego wyścigu.

Niestety powyższy kod nie pozwala na to, ponieważ używamy tego samego odwołania do zamka; Oznacza to, że wątki, nawet jeśli nie są w stanie wyścigu, powinny czekać i oczywiście kod poświęca współbieżność programu.

Rozwiązaniem jest użycie 2 różnych blokad dla dwóch różnych zmiennych:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

W powyższym przykładzie zastosowano bardziej drobnoziarniste zamki (2 zamki zamiast jednego ( lockA i lockB odpowiednio dla zmiennych a i b ), a w rezultacie pozwala na lepszą współbieżność, z drugiej strony stał się bardziej złożony niż pierwszy przykład ...

Andreas Bakurov
źródło
To jest bardzo niebezpieczne. Wprowadzono teraz wymóg zamawiania zamków po stronie klienta (użytkownik tej klasy). Jeśli dwa wątki wywołują metodę1 () i metodę2 () w innej kolejności, prawdopodobnie zakleszczą się, ale użytkownik tej klasy nie ma pojęcia, że ​​tak jest.
daveb
7
Granulacja, której nie zapewnia „synchronizowane (to)”, jest poza zakresem mojego pytania. I czy twoje pola blokady nie powinny być ostateczne?
eljenso
10
aby uzyskać impas, powinniśmy wykonać połączenie z bloku zsynchronizowanego przez A do bloku zsynchronizowanego przez B. daveba, mylisz się ...
Andreas Bakurov 14.01.2009
3
O ile wiem, w tym przykładzie nie ma impasu. Przyjmuję, że to tylko pseudokod, ale użyłbym jednej z implementacji java.util.concurrent.locks.Lock jak java.util.concurrent.locks.ReentrantLock
Shawn Vader
15

Chociaż zgadzam się, aby nie ślepo przestrzegać dogmatycznych zasad, to czy scenariusz „kradzieży zamków” wydaje ci się tak ekscentryczny? Wątek rzeczywiście może uzyskać blokadę obiektu „zewnętrznie” (synchronized(theObject) {...} ), blokując inne wątki oczekujące na metody instancji synchronicznej.

Jeśli nie wierzysz w złośliwy kod, zastanów się, że ten kod może pochodzić od stron trzecich (na przykład, jeśli opracujesz jakiś serwer aplikacji).

„Przypadkowa” wersja wydaje się mniej prawdopodobna, ale jak mówią, „zrób coś idiotycznego, a ktoś wymyśli lepszego idioty”.

Zgadzam się więc ze szkołą myślenia „zależy to od klasy”.


Edytuj następujące 3 pierwsze komentarze eljenso:

Nigdy nie spotkałem się z problemem kradzieży zamka, ale oto wyobrażony scenariusz:

Powiedzmy, że twój system to kontener serwletów, a przedmiotem, który rozważamy, jest ServletContextimplementacja. Ta getAttributemetoda musi być bezpieczna dla wątków, ponieważ atrybuty kontekstu są danymi współużytkowanymi; więc deklarujesz to jakosynchronized . Wyobraźmy sobie również, że udostępniasz publiczną usługę hostingową w oparciu o implementację kontenera.

Jestem twoim klientem i wdrażam mój „dobry” serwlet na swojej stronie. Zdarza się, że mój kod zawiera wywołaniegetAttribute .

Haker, przebrany za innego klienta, wdraża swój złośliwy serwlet na twojej stronie. Zawiera następujący kod w plikuinit metodzie:

synchronized (this.getServletConfig (). getServletContext ()) {
   while (true) {}
}

Zakładając, że dzielimy ten sam kontekst serwletu (dozwolony przez specyfikację, o ile dwa serwlety znajdują się na tym samym wirtualnym hoście), moje wywołanie getAttributejest zablokowane na zawsze. Haker osiągnął DoS na moim serwletu.

Ten atak nie jest możliwy, jeśli getAttributejest zsynchronizowany z blokadą prywatną, ponieważ kod innej firmy nie może uzyskać tej blokady.

Przyznaję, że ten przykład jest wymyślony i zbyt uproszczony pogląd na to, jak działa kontener serwletu, ale IMHO to potwierdza.

Wybrałbym więc projekt w oparciu o względy bezpieczeństwa: czy będę mieć pełną kontrolę nad kodem, który ma dostęp do instancji? Jakie byłyby konsekwencje trzymania przez wątek blokady na instancji w nieskończoność?

Olivier
źródło
to zależy od tego, co robi klasa: jeśli jest to „ważny” obiekt, to zablokuj prywatne odniesienie? Czy wystarczy blokowanie instancji?
eljenso
6
Tak, scenariusz kradzieży zamków wydaje mi się zbyt trudny. Wszyscy o tym wspominają, ale kto to zrobił lub doświadczył? Jeśli „przypadkowo” zablokujesz obiekt, którego nie powinieneś, to jest nazwa dla tego typu sytuacji: to błąd. Napraw to.
eljenso
4
Również blokowanie wewnętrznych odniesień nie jest wolne od „zewnętrznego ataku synchronizującego”: jeśli wiesz, że pewna zsynchronizowana część kodu czeka na zdarzenie zewnętrzne (np. Zapis pliku, wartość w DB, zdarzenie timera), prawdopodobnie załóż, aby to również blokowało.
eljenso
Pozwólcie, że wyznaję, że jestem jednym z tych idiotów, choć zrobiłem to, gdy byłem młody. Myślałem, że kod jest czystszy, ponieważ nie tworzy jawnego obiektu blokady, a zamiast tego użyłem innego prywatnego obiektu końcowego, który musiał uczestniczyć w monitorze. Nie wiedziałem, że sam obiekt sam się zsynchronizował. Możecie sobie wyobrazić wynikający z tego hijinx ...
Alan Cabrera,
12

To zależy od sytuacji.
Jeśli istnieje tylko jedna jednostka udostępniania lub więcej niż jedna.

Zobacz pełny przykład działania tutaj

Małe wprowadzenie.

Wątki i elementy
możliwe do udostępnienia Dostęp do tego samego elementu może mieć wiele wątków, np. Wiele wątków połączenia udostępniających jedną wiadomość. Ponieważ wątki działają jednocześnie, może istnieć szansa na zastąpienie danych innymi, co może być błędem.
Potrzebujemy więc sposobu, aby zapewnić dostęp do elementu udostępnianego tylko przez jeden wątek na raz. (KONKURENCJA).

Blok
zsynchronizowany blok zsynchronizowany () jest sposobem na zapewnienie równoczesnego dostępu do jednostki współdzielonej.
Po pierwsze, mała analogia
Załóżmy, że w łazience są dwie osoby P1, P2 (wątki) Umywalka (wspólny obiekt) i są drzwi (zamek).
Teraz chcemy, aby jedna osoba korzystała z umywalki na raz.
Podejście polega na zablokowaniu drzwi przez P1, gdy drzwi są zamknięte P2 P2 czeka, aż p1 zakończy swoją pracę
P1 odblokuje drzwi,
wtedy tylko p1 może korzystać z umywalki.

składnia.

synchronized(this)
{
  SHARED_ENTITY.....
}

„to” zapewniło wewnętrzną blokadę związaną z klasą (programista Java zaprojektował klasę Object w taki sposób, że każdy obiekt może działać jako monitor). Powyższe podejście działa dobrze, gdy istnieje tylko jeden wspólny byt i wiele wątków (1: N). N współużytkowanych podmiotów - wątki M Pomyślmy teraz o sytuacji, gdy w umywalni znajdują się dwie umywalki i tylko jedne drzwi. Jeśli stosujemy poprzednie podejście, tylko p1 może korzystać z jednej umywalki na raz, podczas gdy p2 będzie czekać na zewnątrz. Jest to marnotrawstwo zasobów, ponieważ nikt nie używa B2 (umywalki). Mądrzejszym rozwiązaniem byłoby stworzenie mniejszego pokoju w łazience i zapewnienie im jednych drzwi na umywalkę. W ten sposób P1 może uzyskać dostęp do B1, a P2 może uzyskać dostęp do B2 i odwrotnie.
wprowadź opis zdjęcia tutaj

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

wprowadź opis zdjęcia tutaj
wprowadź opis zdjęcia tutaj

Zobacz więcej na Wątki ----> tutaj

Rohit Singh
źródło
11

W obozach C # i Java wydaje się inny konsensus. Większość kodu Java, który widziałem, wykorzystuje:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

mając na uwadze, że większość kodu C # wybiera prawdopodobnie bezpieczniejsze:

// instance level lock object
private readonly object _syncObj = new object();

...

// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
    // do work here
}

Idiom C # jest z pewnością bezpieczniejszy. Jak wspomniano wcześniej, nie można uzyskać złośliwego / przypadkowego dostępu do zamka spoza instancji. Kod Java również ma to ryzyko, ale wydaje się, że społeczność Java z czasem przeszła do nieco mniej bezpiecznej, ale nieco bardziej zwięzłej wersji.

To nie ma oznaczać kopania w Javie, tylko odzwierciedlenie mojego doświadczenia w pracy w obu językach.

serg10
źródło
3
Być może ponieważ C # jest młodszym językiem, nauczyli się od złych wzorców, które wymyślono w obozie Java i lepiej kodują takie rzeczy? Czy jest też mniej singletonów? :)
Bill K
3
On on Być może to prawda, ale nie zamierzam wychodzić na przynętę! Można z
całą
1
Po prostu nieprawda (
delikatnie mówiąc
7

java.util.concurrentPakiet znacznie zmniejsza złożoność mojego bezpiecznego kodu gwintu. Mam tylko niepotwierdzone dowody na kontynuację, ale większość pracy, jaką widziałem, synchronized(x)wydaje się polegać na ponownym wdrażaniu blokady, semafora lub zatrzasku, ale przy użyciu monitorów niższego poziomu.

Mając to na uwadze, synchronizacja przy użyciu dowolnego z tych mechanizmów jest analogiczna do synchronizacji na obiekcie wewnętrznym, a nie przecieka blokady. Jest to korzystne, ponieważ masz absolutną pewność, że kontrolujesz wejście do monitora za pomocą dwóch lub więcej wątków.

jamesh
źródło
6
  1. Uczyń swoje dane niezmiennymi, jeśli jest to możliwe ( finalzmienne)
  2. Jeśli nie możesz uniknąć mutacji wspólnych danych w wielu wątkach, użyj konstrukcji programistycznych wysokiego poziomu [np. Szczegółowy Lockinterfejs API]

Blokada zapewnia wyłączny dostęp do współdzielonego zasobu: tylko jeden wątek może uzyskać blokadę na raz, a cały dostęp do zasobu współdzielonego wymaga, aby najpierw uzyskać blokadę.

Przykładowy kod do użycia, ReentrantLockktóry implementuje Lockinterfejs

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

Zalety Lock over Synchronized (to)

  1. Zastosowanie zsynchronizowanych metod lub instrukcji wymusza, aby wszystkie pozyskiwanie i zwalnianie blokady odbywało się w sposób blokowy.

  2. Implementacje blokady zapewniają dodatkową funkcjonalność w stosunku do korzystania ze zsynchronizowanych metod i instrukcji

    1. Nieblokująca próba uzyskania blokady ( tryLock())
    2. Próba uzyskania blokady, którą można przerwać ( lockInterruptibly())
    3. Próba uzyskania blokady, która może przekroczyć limit czasu ( tryLock(long, TimeUnit)).
  3. Klasa Lock może również zapewniać zachowanie i semantykę, które są zupełnie inne niż niejawna blokada monitora, na przykład

    1. gwarantowane zamówienie
    2. użycie bez ponownej rejestracji
    3. Wykrywanie zakleszczenia

Spójrz na to pytanie SE dotyczące różnych rodzajów Locks:

Synchronizacja vs Blokada

Bezpieczeństwo wątków można osiągnąć, używając zaawansowanego interfejsu API współbieżności zamiast bloków synchronicznych. Ta strona dokumentacji zawiera dobre konstrukcje programistyczne do osiągnięcia bezpieczeństwa wątków.

Zablokuj obiekty obsługują idiomy blokujące, które upraszczają wiele jednoczesnych aplikacji.

Wykonawcy definiują interfejs API wysokiego poziomu do uruchamiania wątków i zarządzania nimi. Implementacje executorów dostarczone przez java.util.concurrent zapewniają zarządzanie pulą wątków odpowiednie dla aplikacji na dużą skalę.

Współbieżne kolekcje ułatwiają zarządzanie dużymi zbiorami danych i mogą znacznie zmniejszyć potrzebę synchronizacji.

Zmienne atomowe mają funkcje minimalizujące synchronizację i pomagające uniknąć błędów spójności pamięci.

ThreadLocalRandom (w JDK 7) zapewnia wydajne generowanie liczb pseudolosowych z wielu wątków.

Zapoznaj się także z pakietami java.util.concurrent i java.util.concurrent.atomic, aby zapoznać się z innymi konstrukcjami programistycznymi.

Ravindra babu
źródło
5

Jeśli zdecydowałeś, że:

  • musisz zablokować bieżący obiekt; i
  • chcesz to zablokować z ziarnistością mniejszą niż cała metoda;

wtedy nie widzę tabu zsynchronizowanego (this).

Niektóre osoby celowo używają synchronizacji (tej) (zamiast oznaczania metody synchronizowanej) w całej zawartości metody, ponieważ uważają, że „czytelnik jest bardziej zrozumiały”, na którym obiekcie jest faktycznie synchronizowany. Tak długo, jak ludzie dokonują świadomego wyboru (np. Rozumieją, że robiąc to, w rzeczywistości wstawiają dodatkowe kody bajtowe do metody, co może mieć wpływ na potencjalne optymalizacje), nie widzę w tym szczególnie problemu . Zawsze powinieneś dokumentować równoczesne zachowanie twojego programu, więc nie widzę argumentu „zsynchronizowane” publikuje zachowanie jako tak przekonującego.

Jeśli chodzi o pytanie, którą blokadę obiektu powinieneś użyć, myślę, że nie ma nic złego w synchronizacji na bieżącym obiekcie, jeśli byłoby to oczekiwane na podstawie logiki tego, co robisz i tego, w jaki sposób klasa byłaby zwykle używana . Na przykład w przypadku kolekcji obiekt, którego logicznie można się spodziewać, to zazwyczaj sama kolekcja.

Neil Coffey
źródło
1
„jeśli logika tego oczekiwałaby ...” to punkt, który staram się również rozwiązać. Nie widzę sensu, aby zawsze używać prywatnych zamków, chociaż wydaje się, że ogólny konsensus jest lepszy, ponieważ nie boli i jest bardziej defensywny.
eljenso
4

Myślę, że istnieje dobre wytłumaczenie, dlaczego każda z tych technik jest ważna dla Ciebie w książce Brian Goetz zatytułowanej Java Concurrency In Practice. Wyjaśnia jeden punkt bardzo jasno - musisz użyć tego samego zamka „WSZĘDZIE”, aby chronić stan swojego obiektu. Metoda zsynchronizowana i synchronizacja na obiekcie często idą w parze. Np. Vector synchronizuje wszystkie swoje metody. Jeśli masz uchwyt do obiektu wektorowego i zamierzasz wykonać polecenie „wstaw, jeśli jest nieobecny”, to jedynie synchronizacja wektorowa własnych metod nie ochroni cię przed uszkodzeniem stanu. Musisz zsynchronizować za pomocą synchronizowanego (vectorHandle). Spowoduje to, że blokada SAME zostanie przejęta przez każdy wątek z uchwytem do wektora i ochroni ogólny stan wektora. Nazywa się to blokowaniem po stronie klienta. Wiemy, że wektor synchronizuje (to) / synchronizuje wszystkie swoje metody, a zatem synchronizacja na obiekcie vectorHandle spowoduje prawidłową synchronizację stanu obiektów wektorowych. Głupotą jest wierzyć, że jesteś bezpieczny dla wątków tylko dlatego, że używasz kolekcji dla wątków. Właśnie z tego powodu ConcurrentHashMap wyraźnie wprowadził metodę putIfAbsent - aby takie operacje były atomowe.

W podsumowaniu

  1. Synchronizacja na poziomie metody umożliwia blokowanie po stronie klienta.
  2. Jeśli masz prywatny obiekt blokady - uniemożliwia to blokowanie po stronie klienta. Jest to w porządku, jeśli wiesz, że twoja klasa nie ma typu „wstaw, jeśli nieobecny”.
  3. Jeśli projektujesz bibliotekę - synchronizacja na tym lub synchronizacja metody jest często mądrzejsza. Ponieważ rzadko jesteś w stanie zdecydować, w jaki sposób zostanie wykorzystana twoja klasa.
  4. Gdyby Vector użył prywatnego obiektu zamka - nie byłoby możliwe prawidłowe ustawienie „nieobecności”. Kod klienta nigdy nie uzyska uchwytu do prywatnej blokady, co złamie podstawową zasadę korzystania z EXACT SAME LOCK w celu ochrony jego stanu.
  5. Synchronizacja przy użyciu tej lub zsynchronizowanych metod ma problem, jak zauważyli inni - ktoś może dostać blokadę i nigdy jej nie zwolnić. Wszystkie pozostałe wątki czekałyby na zwolnienie blokady.
  6. Wiedz więc, co robisz, i zastosuj ten, który jest poprawny.
  7. Ktoś argumentował, że posiadanie prywatnego obiektu zamka zapewnia lepszą szczegółowość - np. Jeśli dwie operacje nie są ze sobą powiązane - mogą być chronione przez różne zamki, co skutkuje lepszą przepustowością. Ale myślę, że jest to zapach projektowy, a nie zapachowy - jeśli dwie operacje są całkowicie niezwiązane, dlaczego należą do klasy SAME? Dlaczego w ogóle klub klasy miałby mieć niepowiązane funkcjonalności? Może być klasą użyteczności? Hmmmm - niektóre zastosowania zapewniają manipulację ciągami i formatowanie daty kalendarzowej za pośrednictwem tego samego wystąpienia? ... przynajmniej nie ma dla mnie sensu !!
Sandesh Sadhale
źródło
3

Nie, nie zawsze powinieneś . Jednak staram się go unikać, gdy istnieje wiele obaw dotyczących konkretnego obiektu, które muszą być bezpieczne tylko dla siebie. Na przykład, możesz mieć zmienny obiekt danych, który ma pola „label” i „parent”; te muszą być bezpieczne dla wątków, ale zmiana jednego nie musi blokować drugiego zapisu i odczytu. (W praktyce unikałbym tego, deklarując zmienność pól i / lub używając opakowań AtomicFoo z java.util.concurrent).

Synchronizacja w ogóle jest nieco niezdarna, ponieważ uderza w dużą blokadę, zamiast myśleć dokładnie, jak wątki mogą ze sobą współpracować. Używanie synchronized(this)jest jeszcze bardziej nieporadne i antyspołeczne, ponieważ mówi „nikt nie może nic zmienić w tej klasie, dopóki trzymam zamek”. Jak często musisz to robić?

Wolałbym mieć bardziej szczegółowe zamki; nawet jeśli chcesz zatrzymać wszystko przed zmianą (być może szeregujesz obiekt), możesz po prostu zdobyć wszystkie blokady, aby osiągnąć to samo, a dodatkowo jest to bardziej wyraźne. Kiedy używasz synchronized(this), nie jest jasne, dlaczego synchronizujesz ani jakie mogą być skutki uboczne. Jeśli używasz synchronized(labelMonitor), a nawet lepiejlabelLock.getWriteLock().lock() , jasne jest, co robisz i do czego ograniczone są efekty twojej krytycznej sekcji.

Andrzej Doyle
źródło
3

Krótka odpowiedź : musisz zrozumieć różnicę i dokonać wyboru w zależności od kodu.

Długa odpowiedź : ogólnie wolałbym unikać synchronizacji (tej), aby zmniejszyć rywalizację, ale prywatne zamki zwiększają złożoność, o której musisz wiedzieć. Więc użyj właściwej synchronizacji dla właściwego zadania. Jeśli nie masz doświadczenia w programowaniu wielowątkowym, wolę trzymać się blokowania instancji i poczytać na ten temat. (To powiedziawszy: samo użycie synchronizacji (this) nie powoduje, że twoja klasa jest w pełni bezpieczna dla wątków.) To nie jest łatwy temat, ale kiedy już się przyzwyczaisz, odpowiedź, czy użyć synchronizacji (this), czy nie, przychodzi naturalnie .

tcurdt
źródło
Czy rozumiem cię poprawnie, gdy mówisz, że to zależy od twojego doświadczenia?
eljenso
Przede wszystkim zależy to od kodu, który chcesz napisać. Wystarczy powiedzieć, że możesz potrzebować trochę więcej doświadczenia, gdy odwracasz się, aby nie używać synchronizacji (this).
tcurdt
2

Blokada służy do widoczności lub do ochrony niektórych danych przed jednoczesnymi modyfikacjami które mogą prowadzić do wyścigu.

Gdy potrzebujesz po prostu wykonać operacje typu pierwotnego, aby były atomowe, dostępne są opcje takie jak AtomicInteger i .

Załóżmy jednak, że masz dwie liczby całkowite, które są ze sobą powiązane, takie jak xiy współrzędnych, które są ze sobą powiązane i powinny zostać zmienione w sposób atomowej. Następnie chroniłbyś ich za pomocą tego samego zamka.

Blokada powinna chronić tylko stan, który jest ze sobą powiązany. Nie mniej i nie więcej. Jeśli użyjesz synchronized(this)w każdej metodzie, to nawet jeśli stan klasy nie jest ze sobą powiązany, wszystkie wątki staną w obliczu niezgody, nawet jeśli zaktualizujesz stan niepowiązany.

class Point{
   private int x;
   private int y;

   public Point(int x, int y){
       this.x = x;
       this.y = y;
   }

   //mutating methods should be guarded by same lock
   public synchronized void changeCoordinates(int x, int y){
       this.x = x;
       this.y = y;
   }
}

W powyższym przykładzie mam tylko jedną metodę, która mutuje obie, xa ynie dwie różne metody jako xi ysą powiązane, a gdybym podał dwie różne metody mutacji xi yosobno, nie byłoby to bezpieczne dla wątków.

Ten przykład ma na celu jedynie zademonstrowanie, a niekoniecznie sposób, w jaki należy go wdrożyć. Najlepszym sposobem, aby to zrobić, byłoby uczynienie go NIEZWYKŁA .

Teraz w przeciwieństwie do Pointprzykładu, istnieje przykład TwoCounterspodany już przez @Andreas, w którym stan chroniony przez dwie różne blokady, ponieważ stan nie jest ze sobą powiązany.

Proces korzystania z różnych blokad w celu ochrony stanów niepowiązanych nazywa się Blokowaniem pasków lub Blokowaniem podziału

Narendra Pathai
źródło
1

Powodem nie do synchronizowania na to , że czasem trzeba więcej niż jednego zamka (drugiego zamka często zostaje usunięty po pewnym dodatkowym myślenia, ale nadal trzeba go w stanie pośrednim). Jeśli zablokujesz na to , że zawsze trzeba pamiętać, której jeden z dwóch zamków jest to ; jeśli zablokujesz prywatny obiekt, nazwa zmiennej ci o tym powie.

Z punktu widzenia czytelnika, jeśli widzisz blokowanie tego , zawsze musisz odpowiedzieć na dwa pytania:

  1. jaki rodzaj dostępu jest przez to chroniony ?
  2. czy jeden zamek naprawdę wystarczy, czy ktoś nie wprowadził błędu?

Przykład:

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

Jeśli dwa wątki zaczynają się longOperation()w dwóch różnych przypadkach BadObject, nabywają blokady; kiedy nadszedł czas na wywołaniel.onMyEvent(...) , mamy impas, ponieważ żaden z wątków nie może uzyskać blokady drugiego obiektu.

W tym przykładzie możemy wyeliminować zakleszczenie, używając dwóch zamków, jednego dla krótkich operacji i jednego dla długich.

18446744073709551615
źródło
2
Jedynym sposobem na uzyskanie impasu w tym przykładzie jest BadObjectwywołanie A longOperationprzez B, przekazanie A myListeneri odwrotnie. Nie niemożliwe, ale dość skomplikowane, potwierdzając moje wcześniejsze uwagi.
eljenso
1

Jak już powiedziano tutaj, zsynchronizowany blok może wykorzystywać zmienną zdefiniowaną przez użytkownika jako obiekt blokady, gdy funkcja synchronizacji używa tylko „tego”. I oczywiście możesz manipulować obszarami swojej funkcji, które powinny być zsynchronizowane i tak dalej.

Ale wszyscy mówią, że nie ma różnicy między funkcją zsynchronizowaną a blokiem, który obejmuje całą funkcję, używając „tego” jako obiektu blokady. To nieprawda, różnica polega na kodzie bajtów, który zostanie wygenerowany w obu sytuacjach. W przypadku użycia bloku zsynchronizowanego należy przypisać zmienną lokalną, która zawiera odniesienie do „tego”. W rezultacie będziemy mieli nieco większy rozmiar funkcji (nie dotyczy, jeśli masz tylko kilka funkcji).

Bardziej szczegółowe wyjaśnienie różnicy można znaleźć tutaj: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Również użycie zsynchronizowanego bloku nie jest dobre ze względu na następujący punkt widzenia:

Zsynchronizowane słowo kluczowe jest bardzo ograniczone w jednym obszarze: przy wychodzeniu ze zsynchronizowanego bloku wszystkie wątki oczekujące na tę blokadę muszą zostać odblokowane, ale tylko jeden z tych wątków może przejąć blokadę; wszyscy inni widzą, że blokada została przejęta i wracają do stanu zablokowanego. To nie tylko wiele zmarnowanych cykli przetwarzania: często przełącznik kontekstowy w celu odblokowania wątku obejmuje również stronicowanie pamięci z dysku, a to jest bardzo, bardzo, drogie.

Aby uzyskać więcej informacji w tym obszarze, polecam przeczytanie tego artykułu: http://java.dzone.com/articles/synchronized-consisted

rzymski
źródło
1

Jest to tak naprawdę tylko uzupełnienie innych odpowiedzi, ale jeśli twoim głównym sprzeciwem wobec używania prywatnych obiektów do blokowania jest to, że zaśmieca twoją klasę polami, które nie są związane z logiką biznesową, wtedy Project Lombok musi @Synchronizedwygenerować płytkę zbiorczą w czasie kompilacji:

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

kompiluje się do

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

public int foo() {
    synchronized($lock) {
        return 0;
    }
}
Michał
źródło
0

Dobry przykład zastosowania zsynchronizowanego (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...
}

Jak widać tutaj, używamy do tego synchronizacji, aby łatwo współpracować metodą długiej (być może nieskończonej pętli uruchamiania) z niektórymi tam metodami synchronizowanymi.

Oczywiście można go bardzo łatwo przepisać przy użyciu synchronizacji na prywatnym polu. Ale czasami, kiedy mamy już pewne projektowanie z metodami zsynchronizowanymi (tj. Starszą klasę, z której wywodzimy, zsynchronizowane (to) może być jedynym rozwiązaniem).

Bart Prokop
źródło
Dowolny przedmiot może być tutaj użyty jako zamek. Nie musi tak być this. To może być pole prywatne.
finnw
Zgadza się, ale celem tego przykładu było pokazanie, jak przeprowadzić właściwą synchronizację, jeśli zdecydujemy się na synchronizację metod.
Bart Prokop
0

To zależy od zadania, które chcesz wykonać, ale nie użyłbym tego. Sprawdź także, czy nie można wykonać zapisywania wątku, którego chcesz osiągnąć, synchronizując (to)? W interfejsie API jest też kilka fajnych blokad, które mogą ci pomóc :)

Harald Schilly
źródło
0

Chcę tylko wspomnieć o możliwym rozwiązaniu dla unikatowych prywatnych odniesień w atomowych częściach kodu bez zależności. Możesz użyć statycznej Hashmapy z blokadami i prostej metody statycznej o nazwie atomic (), która automatycznie tworzy wymagane odwołania przy użyciu informacji o stosie (pełna nazwa klasy i numer linii). Następnie możesz użyć tej metody do synchronizacji instrukcji bez pisania nowego obiektu blokady.

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}
Kolarčík Václav
źródło
0

Unikaj używania synchronized(this)jako mechanizmu blokującego: blokuje to instancję całej klasy i może powodować zakleszczenia. W takich przypadkach refaktoryzuj kod, aby zablokować tylko określoną metodę lub zmienną, w ten sposób cała klasa nie zostanie zablokowana. Synchronisedmoże być używany wewnątrz poziomu metody.
Zamiast używać synchronized(this)poniższy kod pokazuje, jak możesz po prostu zablokować metodę.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }
surendrapanday
źródło
0

Moje dwa centy w 2019 r., Chociaż to pytanie można było już rozwiązać.

Blokowanie „tego” nie jest złe, jeśli wiesz, co robisz, ale poza sceną blokowanie „tego” jest (co niestety pozwala na to zsynchronizowane słowo kluczowe w definicji metody).

Jeśli naprawdę chcesz, aby użytkownicy twojej klasy mogli „ukraść” twoją blokadę (tj. Uniemożliwić radzenie sobie z nią przez inne wątki), tak naprawdę chcesz, aby wszystkie zsynchronizowane metody czekały na uruchomienie innej metody synchronizacji i tak dalej. Powinien być celowy i przemyślany (a zatem udokumentowany, aby pomóc użytkownikom w zrozumieniu tego).

W celu dalszego rozwinięcia, na odwrót, musisz wiedzieć, co „zyskujesz” (lub „tracisz” na), jeśli zablokujesz niedostępny zamek (nikt nie może „ukraść” zamka, masz całkowitą kontrolę i tak dalej. ..).

Problemem jest dla mnie to, że zsynchronizowane słowo kluczowe w podpisie definicji metody sprawia, że ​​programiści po prostu zbyt łatwo nie zastanawiają się, co zablokować, co jest bardzo ważną rzeczą do przemyślenia, jeśli nie chcesz napotkać problemów w wielu -działany program.

Nie można argumentować, że „zwykle” nie chcesz, aby użytkownicy z twojej klasy mogli robić te rzeczy lub że „zwykle” chcesz ... Zależy to od tego, jaką funkcjonalność kodujesz. Nie można wprowadzić reguły kciuka, ponieważ nie można przewidzieć wszystkich przypadków użycia.

Rozważ np. Drukarkę, która korzysta z wewnętrznej blokady, ale wtedy ludzie mają trudności z użyciem jej z wielu wątków, jeśli nie chcą, aby ich wyniki się przeplatały.

Jeśli twoja blokada jest dostępna poza klasą, czy nie, to twoja decyzja jako programisty zależy od tego, jakie funkcje ma klasa. Jest to część interfejsu API. Nie można na przykład odejść od zsynchronizowanego (this) do zsynchronizowanego (provateObjet) bez ryzyka zerwania zmian w kodzie przy jego użyciu.

Uwaga 1: Wiem, że możesz osiągnąć wszystko, co zsynchronizowane (to) „osiąga”, używając jawnego obiektu blokady i ujawniając go, ale myślę, że nie jest konieczne, jeśli twoje zachowanie jest dobrze udokumentowane i naprawdę wiesz, co oznacza blokowanie na „to”.

Uwaga 2: Nie zgadzam się z argumentem, że jeśli jakiś kod przypadkowo kradnie blokadę, jest to błąd i musisz go rozwiązać. Jest to w pewnym sensie ten sam argument, co stwierdzenie, że mogę upublicznić wszystkie moje metody, nawet jeśli nie mają one być publiczne. Jeśli ktoś „przypadkowo” dzwoni do mojej prywatnej metody, to jest to błąd. Po co w ogóle włączać ten wypadek !!! Jeśli umiejętność kradzieży zamka jest problemem dla twojej klasy, nie pozwalaj na to. Tak proste jak to.

Amit Mittal
źródło
-3

Myślę, że punkty jeden (ktoś inny używa twojej blokady) i dwa (wszystkie metody niepotrzebnie wykorzystujące tę samą blokadę) mogą się zdarzyć w każdej dość dużej aplikacji. Zwłaszcza, gdy nie ma dobrej komunikacji między programistami.

Nie jest odlewany w kamieniu, to głównie kwestia dobrych praktyk i zapobiegania błędom.

Yoni Roit
źródło