Jaka jest różnica między atomowym / lotnym / zsynchronizowanym?

297

Jak atomowy / lotny / zsynchronizowany działa wewnętrznie?

Jaka jest różnica między następującymi blokami kodu?

Kod 1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Kod 2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

Kod 3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Czy volatiledziała w następujący sposób? Jest

volatile int i = 0;
void incIBy5() {
    i += 5;
}

równoważny

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

Myślę, że dwa wątki nie mogą jednocześnie wejść do zsynchronizowanego bloku ... mam rację? Jeśli to prawda, to jak to atomic.incrementAndGet()działa bez synchronized? I czy jest bezpieczny dla wątków?

Jaka jest różnica między wewnętrznym odczytem a zapisem do zmiennych lotnych / zmiennych atomowych? W pewnym artykule przeczytałem, że wątek ma lokalną kopię zmiennych - co to jest?

hardik
źródło
5
To powoduje wiele pytań, a kod nawet się nie kompiluje. Może powinieneś przeczytać dobrą książkę, na przykład Java Concurrency in Practice.
JB Nizet,
4
@JBNizet masz rację !!! mam tę książkę, w skrócie nie mam pojęcia o atomie i nie mam pojęcia o tym. przekleństwa to mój błąd, nie autora.
hardik
4
Tak naprawdę nie musisz dbać o to, jak to jest zaimplementowane (i zależy to od systemu operacyjnego). Musisz zrozumieć umowę: wartość jest zwiększana atomowo, a wszystkie pozostałe wątki mają gwarancję zobaczenia nowej wartości.
JB Nizet,

Odpowiedzi:

392

Pytasz w szczególności o to, jak działają wewnętrznie , więc oto:

Brak synchronizacji

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Zasadniczo odczytuje wartość z pamięci, zwiększa ją i przywraca do pamięci. Działa to w jednym wątku, ale obecnie, w dobie wielordzeniowych, wieloprocesorowych i wielopoziomowych pamięci podręcznych, nie będzie działać poprawnie. Przede wszystkim wprowadza warunek wyścigu (kilka wątków może odczytać wartość w tym samym czasie), ale także problemy z widocznością. Wartość może być przechowywana tylko w „ lokalnej ” pamięci procesora (część pamięci podręcznej) i nie może być widoczna dla innych procesorów / rdzeni (a zatem - wątków). Dlatego wiele osób odnosi się do lokalnej kopii zmiennej w wątku. To jest bardzo niebezpieczne. Rozważ ten popularny, ale zepsuty kod zatrzymujący wątek:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

Dodaj volatiledo stoppedzmiennej i działa dobrze - jeśli jakikolwiek inny wątek zmodyfikuje stoppedzmienną pleaseStop()metodą, masz gwarancję, że zobaczysz tę zmianę natychmiast w while(!stopped)pętli działającego wątku . BTW to nie jest dobry sposób na przerwanie wątku, zobacz: Jak zatrzymać wątek działający na zawsze bez użycia i Zatrzymywanie określonego wątku Java .

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

AtomicIntegerZastosowania klasy CAS ( compare-and-swap ) operacje niskiego poziomu CPU (brak synchronizacji potrzebne!) Pozwalają one modyfikować konkretnej zmiennej tylko wtedy, gdy wartość bieżąca jest równa coś innego (i jest zwracany z powodzeniem). Więc kiedy go wykonasz getAndIncrement(), faktycznie działa w pętli (uproszczona rzeczywista implementacja):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

Więc w zasadzie: czytaj; spróbuj zapisać wartość przyrostową; jeśli się nie powiedzie (wartość nie jest już równa current), przeczytaj i spróbuj ponownie. compareAndSet()Jest realizowany w kodzie natywnym (montaż).

volatile bez synchronizacji

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Ten kod jest nieprawidłowy. Naprawiono problem z widocznością ( volatileupewnia się, że inne wątki mogą zobaczyć zmiany wprowadzone counter), ale nadal ma warunki wyścigu. Zostało to wyjaśnione wiele razy: inkrementacja przed / po nie jest atomowa.

Jedynym efektem ubocznym volatilejest „ opróżnianie ” pamięci podręcznej, aby wszystkie inne podmioty widziały najświeższą wersję danych. W większości sytuacji jest to zbyt surowe; dlatego volatilenie jest domyślny.

volatile bez synchronizacji (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

Ten sam problem jak powyżej, ale jeszcze gorzej, ponieważ inie jest private. Wyścig jest nadal obecny. Dlaczego to jest problem? Jeśli powiedzmy, że dwa wątki uruchamiają ten kod jednocześnie, wynikiem może być + 5lub + 10. Masz jednak gwarancję, że zobaczysz zmianę.

Wiele niezależnych synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

Niespodzianka, ten kod jest również niepoprawny. W rzeczywistości jest to całkowicie błędne. Przede wszystkim synchronizujesz i, który ma się zmienić (co więcej, ijest prymitywny, więc myślę, że synchronizujesz tymczasowy Integerutworzony przez autoboxing ...) Całkowicie wadliwy. Możesz także napisać:

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

Żadne dwa wątki nie mogą wejść do tego samego synchronizedbloku z tym samym zamkiem . W tym przypadku (i podobnie w kodzie) obiekt blokady zmienia się przy każdym wykonaniu, więc synchronizedskutecznie nie ma żadnego efektu.

Nawet jeśli użyłeś ostatniej zmiennej (lub this) do synchronizacji, kod jest nadal niepoprawny. Dwa wątki mogą najpierw zapoznać isię tempsynchronicznie (o tej samej wartości lokalnie temp), to pierwszy przypisuje nową wartość i(powiedzmy od 1 do 6), a drugi robi to samo (od 1 do 6).

Synchronizacja musi rozciągać się od odczytu do przypisania wartości. Twoja pierwsza synchronizacja nie ma żadnego efektu (czytanie intjest atomowa), a także druga. Moim zdaniem są to prawidłowe formularze:

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}
Tomasz Nurkiewicz
źródło
10
Dodam tylko, że JVM kopiuje wartości zmiennych do rejestrów, aby na nich operować. Oznacza to, że wątki działające na jednym procesorze / rdzeniu mogą nadal widzieć różne wartości dla zmiennej nieulotnej.
David Harkness,
@ thomasz: jest zsynchronizowany i zestaw (bieżący, bieżący + 1)? jeśli nie, co się stanie, gdy dwa wątki wykonają tę metodę w tym samym czasie?
hardik
@Hardik: compareAndSetto tylko cienkie opakowanie wokół operacji CAS. W odpowiedzi podaję kilka szczegółów.
Tomasz Nurkiewicz
1
@thomsasz: ok, i przejść przez ten łącza pytanie i odpowiedział Jon Skeet, mówi „gwint nie może odczytać zmienną lotny bez sprawdzania, czy każdy inny wątek wykonał write”. ale co się stanie, jeśli jeden wątek jest pomiędzy operacją zapisu, a drugi wątek go odczytuje !! czy się mylę ?? czy nie jest to warunek wyścigu podczas operacji atomowej?
hardik
3
@Hardik: utwórz kolejne pytanie, aby uzyskać więcej odpowiedzi na to, o co pytasz, tutaj to tylko ty i ja, a komentarze nie są odpowiednie do zadawania pytań. Nie zapomnij zamieścić tutaj linku do nowego pytania, abym mógł kontynuować.
Tomasz Nurkiewicz
61

Uznanie zmiennej za niestabilną oznacza, że ​​modyfikacja jej wartości natychmiast wpływa na faktyczne miejsce w pamięci dla zmiennej. Kompilator nie może zoptymalizować żadnych odniesień do zmiennej. Gwarantuje to, że gdy jeden wątek zmodyfikuje zmienną, wszystkie pozostałe wątki natychmiast zobaczą nową wartość. (Nie jest to gwarantowane w przypadku zmiennych nielotnych.)

Zadeklarowanie zmiennej atomowej gwarantuje, że operacje wykonane na zmiennej zachodzą w sposób atomowy, tj. Że wszystkie podetapy operacji są zakończone w wątku, w którym są wykonywane i nie są przerywane przez inne wątki. Na przykład operacja inkrementacji i testowania wymaga inkrementacji zmiennej, a następnie porównania z inną wartością; operacja atomowa gwarantuje, że oba te etapy zostaną wykonane tak, jakby były pojedynczą niepodzielną / nieprzerwaną operacją.

Synchronizacja wszystkich dostępów do zmiennej pozwala na dostęp tylko do jednego wątku na raz i zmusza wszystkie pozostałe wątki do oczekiwania na dostęp do wątku, który zwolni dostęp do zmiennej.

Dostęp zsynchronizowany jest podobny do dostępu atomowego, ale operacje atomowe są generalnie realizowane na niższym poziomie programowania. Jest również całkowicie możliwe zsynchronizowanie tylko niektórych dostępów do zmiennej i umożliwienie niezsynchronizowania innych dostępów (np. Zsynchronizowanie wszystkich zapisów do zmiennej, ale żadnego odczytu z niej).

Atomowość, synchronizacja i zmienność są niezależnymi atrybutami, ale zwykle są używane w połączeniu w celu wymuszenia właściwej współpracy wątków w celu uzyskania dostępu do zmiennych.

Dodatek (kwiecień 2016 r.)

Zsynchronizowany dostęp do zmiennej jest zwykle realizowany za pomocą monitora lub semafora . Są to mechanizmy mutex niskiego poziomu (wzajemne wykluczanie), które pozwalają wątkowi przejąć kontrolę wyłącznie nad zmienną lub blokiem kodu, zmuszając wszystkie pozostałe wątki do czekania, jeśli również spróbują uzyskać ten sam muteks. Gdy wątek będący właścicielem zwolni muteks, kolejny wątek może po kolei uzyskać muteks.

Dodatek (lipiec 2016 r.)

Synchronizacja odbywa się na obiekcie . Oznacza to, że wywołanie zsynchronizowanej metody klasy zablokuje thisobiekt wywołania. Zsynchronizowane statycznie metody zablokują Classsam obiekt.

Podobnie, wprowadzenie zsynchronizowanego bloku wymaga zablokowania thisobiektu metody.

Oznacza to, że zsynchronizowana metoda (lub blok) może być wykonywana w wielu wątkach jednocześnie, jeśli blokują one różne obiekty, ale tylko jeden wątek może wykonać metodę synchroniczną (lub blok) jednocześnie dla dowolnego pojedynczego obiektu.

David R. Tribble
źródło
25

lotny:

volatilejest słowem kluczowym. volatilezmusza wszystkie wątki do pobrania najnowszej wartości zmiennej z pamięci głównej zamiast z pamięci podręcznej. Aby uzyskać dostęp do zmiennych zmiennych, nie jest wymagane blokowanie. Wszystkie wątki mogą jednocześnie uzyskiwać dostęp do zmiennej wartości zmiennej.

Korzystanie ze volatilezmiennych zmniejsza ryzyko błędów spójności pamięci, ponieważ każdy zapis do zmiennej lotnej ustanawia relację przed zdarzeniem z kolejnymi odczytami tej samej zmiennej.

Oznacza to, że zmiany w volatilezmiennej są zawsze widoczne dla innych wątków . Co więcej, oznacza to również, że gdy wątek czyta volatilezmienną, widzi nie tylko ostatnią zmianę niestabilności, ale także skutki uboczne kodu, który doprowadził do zmiany .

Kiedy stosować: Jeden wątek modyfikuje dane, a inne wątki muszą odczytać najnowszą wartość danych. Inne wątki podejmą pewne działania, ale nie zaktualizują danych .

AtomicXXX:

AtomicXXXklasy obsługują bezpieczne dla wątków programowanie na pojedynczych zmiennych. Te AtomicXXXklasy (jak AtomicInteger) rozwiązują błędy niespójności pamięci / skutki uboczne modyfikacji zmiennych lotnych, które były dostępne w wielu wątkach.

Kiedy stosować: Wiele wątków może odczytywać i modyfikować dane.

zsynchronizowane:

synchronizedto słowo kluczowe używane do ochrony metody lub bloku kodu. Dzięki synchronizacji metoda ma dwa efekty:

  1. Po pierwsze, niemożliwe synchronizedjest przeplatanie dwóch wywołań metod na tym samym obiekcie. Gdy jeden wątek wykonuje synchronizedmetodę dla obiektu, wszystkie inne wątki, które wywołują synchronizedmetody dla tego samego bloku obiektu (zawieszają wykonywanie), dopóki pierwszy wątek nie zostanie zakończony z obiektem.

  2. Po drugie, kiedy synchronizedmetoda kończy działanie, automatycznie ustanawia relację przed zdarzeniem z każdym kolejnym wywołaniem synchronizedmetody dla tego samego obiektu. Gwarantuje to, że zmiany stanu obiektu są widoczne dla wszystkich wątków.

Kiedy stosować: Wiele wątków może odczytywać i modyfikować dane. Twoja logika biznesowa nie tylko aktualizuje dane, ale także wykonuje operacje atomowe

AtomicXXXjest równoważne, volatile + synchronizedchociaż implementacja jest inna. AmtomicXXXrozszerza volatilezmienne + compareAndSetmetody, ale nie używa synchronizacji.

Powiązane pytania SE:

Różnica między zmienną i zsynchronizowaną w Javie

Volatile boolean vs AtomicBoolean

Dobre artykuły do ​​przeczytania: (Powyższe treści pochodzą z tych stron dokumentacji)

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html

Ravindra babu
źródło
2
Jest to pierwsza odpowiedź, która faktycznie wspomina semantykę opisywanych słów kluczowych / funkcji, które mają znaczenie przed zrozumieniem, w jaki sposób wpływają one na wykonanie kodu. Wyższe głosowane odpowiedzi pomijają ten aspekt.
jhyot
5

Wiem, że dwa wątki nie mogą jednocześnie wejść do bloku synchronizacji

Dwa wątki nie mogą dwa razy wejść do zsynchronizowanego bloku tego samego obiektu. Oznacza to, że dwa wątki mogą wprowadzić ten sam blok na różnych obiektach. To zamieszanie może prowadzić do takiego kodu.

private Integer i = 0;

synchronized(i) {
   i++;
}

Nie będzie to działało zgodnie z oczekiwaniami, ponieważ może za każdym razem blokować inny obiekt.

jeśli jest to prawda niż jak działa ta funkcja atomic.incrementAndGet () bez synchronizacji? i czy wątek jest bezpieczny?

tak. Nie używa blokowania, aby osiągnąć bezpieczeństwo gwintu.

Jeśli chcesz wiedzieć, jak działają bardziej szczegółowo, możesz przeczytać dla nich kod.

Jaka jest różnica między wewnętrznym odczytem a zapisem w zmiennej lotnej / zmiennej atomowej?

Klasa atomowa wykorzystuje zmienne pola. Nie ma różnicy w terenie. Różnica polega na wykonywanych operacjach. Klasy Atomic używają operacji CompareAndSwap lub CAS.

przeczytałem w pewnym artykule, że wątek ma lokalną kopię zmiennych co to jest?

Mogę jedynie założyć, że odnosi się to do faktu, że każdy procesor ma swój własny widok pamięci podręcznej, który może różnić się od każdego innego procesora. Aby mieć pewność, że Twój procesor ma spójny widok danych, musisz zastosować techniki bezpieczeństwa wątków.

Jest to problem tylko wtedy, gdy pamięć jest współdzielona, ​​co najmniej jeden wątek ją aktualizuje.

Peter Lawrey
źródło
@Aniket Thakur jesteś tego pewien? Liczba całkowita jest niezmienna. Więc i ++ prawdopodobnie automatycznie rozpakuje wartość int, zwiększy ją, a następnie utworzy nową liczbę całkowitą, która nie jest tą samą instancją jak poprzednio. Spróbuj zrobić i final, a podczas wywoływania i ++ pojawią się błędy kompilatora.
fuemf5,
2

Synchronized Vs Atomic Vs Volatile:

  • Zmienna i atomowa ma zastosowanie tylko do zmiennej, natomiast metoda zsynchronizowana ma zastosowanie do metody.
  • Lotne zapewniają widoczność, a nie atomowość / spójność obiektu, podczas gdy inne zapewniają widoczność i atomowość.
  • Przechowywanie zmiennych zmiennych w pamięci RAM i szybszy dostęp, ale nie możemy osiągnąć bezpieczeństwa wątku ani synchronizacji bez synchronizowanego słowa kluczowego.
  • Zsynchronizowane zaimplementowane jako zsynchronizowany blok lub metoda zsynchronizowana, podczas gdy obie nie. Możemy wątkować bezpieczny wielokrotny wiersz kodu za pomocą zsynchronizowanego słowa kluczowego, a przy obu nie możemy osiągnąć tego samego.
  • Zsynchronizowane mogą blokować ten sam obiekt klasy lub inny obiekt klasy, podczas gdy oba nie mogą.

Popraw mnie, jeśli coś mi umknęło.

Ajay Gupta
źródło
1

Synchronizacja niestabilna + jest niezawodnym rozwiązaniem dla operacji (instrukcji) w pełni atomowej, która zawiera wiele instrukcji dla procesora.

Powiedz na przykład: lotne int i = 2; i ++, czyli nic innego jak i = i + 1; co powoduje, że i jako wartość 3 w pamięci po wykonaniu tej instrukcji. Obejmuje to odczytanie istniejącej wartości z pamięci dla i (czyli 2), załadowanie do rejestru akumulatora procesora i wykonanie obliczeń poprzez zwiększenie istniejącej wartości o jeden (2 + 1 = 3 w akumulatorze), a następnie zapisanie tej zwiększonej wartości powrót do pamięci. Operacje te nie są wystarczająco atomowe, chociaż wartość i jest niestabilna. Będąc niestabilnym gwarantuje tylko, że POJEDYNCZY odczyt / zapis z pamięci jest atomowy, a nie z WIELU. Dlatego musimy zsynchronizować się także z i ++, aby zachować pewność, że jest to dowód atomowy. Pamiętaj, że instrukcja zawiera wiele instrukcji.

Mam nadzieję, że wyjaśnienie jest wystarczająco jasne.

Thomas Mathew
źródło
1

Zmienny modyfikator Java jest przykładem specjalnego mechanizmu gwarantującego komunikację między wątkami. Kiedy jeden wątek zapisuje zmienną zmienną, a inny wątek widzi, że pisze, pierwszy wątek informuje drugi o całej zawartości pamięci, dopóki nie wykona zapisu do tej zmiennej ulotnej.

Operacje atomowe są wykonywane w jednej jednostce zadania bez ingerencji innych operacji. Operacje atomowe są niezbędne w środowisku wielowątkowym, aby uniknąć niespójności danych.

Sai prateek
źródło