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 volatile
do stopped
zmiennej i działa dobrze - jeśli jakikolwiek inny wątek zmodyfikuje stopped
zmienną 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();
}
AtomicInteger
Zastosowania 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ą ( volatile
upewnia 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 volatile
jest „ 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 volatile
nie 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ż i
nie 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ć + 5
lub + 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, i
jest prymitywny, więc myślę, że synchronizujesz tymczasowy Integer
utworzony 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 synchronized
bloku z tym samym zamkiem . W tym przypadku (i podobnie w kodzie) obiekt blokady zmienia się przy każdym wykonaniu, więc synchronized
skutecznie 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ć i
się temp
synchronicznie (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 int
jest 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;
}
}