Ważne jest, aby zrozumieć, że istnieją dwa aspekty bezpieczeństwa wątków.
- kontrola wykonania oraz
- widoczność pamięci
Pierwszy dotyczy kontrolowania, kiedy kod jest wykonywany (w tym kolejności wykonywania instrukcji) i tego, czy może być wykonywany jednocześnie, a drugi - gdy efekty w pamięci tego, co zostało zrobione, są widoczne dla innych wątków. Ponieważ każdy procesor ma kilka poziomów pamięci podręcznej między nim a pamięcią główną, wątki działające na różnych procesorach lub rdzeniach mogą różnie widzieć „pamięć” w danym momencie, ponieważ wątki mogą uzyskiwać i pracować na prywatnych kopiach pamięci głównej.
Użycie synchronized
zapobiega uzyskaniu monitora (lub blokady) dla tego samego obiektu przez dowolny inny wątek , tym samym uniemożliwiając jednoczesne wykonywanie wszystkich bloków kodu chronionych przez synchronizację na tym samym obiekcie . Synchronizacja tworzy również barierę pamięci „dzieje się przed”, powodując ograniczenie widoczności pamięci, tak że wszystko, co zrobiono do momentu, w którym jakiś wątek zwolni blokadę, pojawia się w innym wątku, który następnie nabył tę samą blokadę , zanim nastąpiło jej nabycie. W praktyce, na obecnym sprzęcie, zazwyczaj powoduje to opróżnianie pamięci podręcznej procesora po zakupie monitora i zapisuje w pamięci głównej po zwolnieniu, które są (stosunkowo) drogie.
volatile
Z drugiej strony użycie zmusza wszystkie wejścia (odczyt lub zapis) do zmiennej zmiennej do wystąpienia w głównej pamięci, skutecznie utrzymując zmienną zmienną poza pamięcią podręczną procesora. Może to być przydatne w przypadku niektórych działań, w których po prostu wymagana jest poprawność widoczności zmiennej, a kolejność dostępu nie jest ważna. Zastosowanie volatile
również zmienia sposób traktowania long
i double
wymaga od nich dostępu atomowego; na niektórych (starszych) urządzeniach może to wymagać blokad, choć nie na nowoczesnym 64-bitowym sprzęcie. Zgodnie z nowym (JSR-133) modelem pamięci dla Java 5+, semantyka lotności została wzmocniona, aby była prawie tak silna, jak zsynchronizowana pod względem widoczności pamięci i kolejności instrukcji (patrz http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile). Dla celów widoczności każdy dostęp do pola niestabilnego działa jak połowa synchronizacji.
W nowym modelu pamięci nadal jest prawdą, że zmienne zmienne nie mogą być ze sobą porządkowane. Różnica polega na tym, że nie jest już tak łatwo zmienić kolejność normalnego dostępu do pola wokół nich. Zapis w polu lotnym ma taki sam efekt pamięci, jak zwolnienie monitora, a odczyt z pola niestabilnego ma taki sam efekt pamięci, jak w przypadku monitora. W efekcie, ponieważ nowy model pamięci nakłada surowsze ograniczenia na zmianę kolejności dostępu do pól lotnych z innymi dostępami do pól, zmiennymi lub nie, wszystko, co było widoczne dla wątku, A
gdy zapisuje w polu lotnym, f
staje się widoczne dla wątku B
podczas odczytu f
.
- JSR 133 (Java Memory Model) FAQ
Tak więc teraz obie formy bariery pamięci (w ramach obecnego JMM) powodują barierę ponownego zamawiania instrukcji, która uniemożliwia kompilatorowi lub czasowi wykonywania ponownego zamówienia instrukcji przez barierę. W starym JMM zmienność nie zapobiegała ponownemu składaniu zamówień. Może to być ważne, ponieważ oprócz barier pamięciowych jedynym ograniczeniem jest to, że dla każdego konkretnego wątku efekt netto kodu jest taki sam, jak gdyby instrukcje były wykonywane dokładnie w kolejności, w jakiej występują w źródło.
Jednym zastosowaniem lotnego jest współdzielony, ale niezmienny obiekt jest odtwarzany w locie, przy czym wiele innych wątków odwołuje się do obiektu w określonym punkcie ich cyklu wykonywania. Potrzebne są inne wątki, aby rozpocząć korzystanie z odtworzonego obiektu po jego opublikowaniu, ale nie potrzeba dodatkowego obciążenia wynikającego z pełnej synchronizacji i towarzyszącego mu sporu i opróżnienia pamięci podręcznej.
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
Mówiąc konkretnie na twoje pytanie odczytu-aktualizacji-zapisu. Rozważ następujący niebezpieczny kod:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Teraz, gdy metoda updateCounter () nie jest zsynchronizowana, dwa wątki mogą do niej wejść jednocześnie. Jedną z wielu kombinacji tego, co może się zdarzyć, jest to, że wątek 1 sprawdza licznik == 1000 i stwierdza, że jest to prawda, a następnie zostaje zawieszony. Następnie wątek 2 wykonuje ten sam test, a także sprawdza, czy jest prawdziwy i jest zawieszony. Następnie wątek-1 wznawia się i ustawia licznik na 0. Następnie wątek-2 wznawia się i ponownie ustawia licznik na 0, ponieważ brakowało aktualizacji z wątku-1. Może się to również zdarzyć, nawet jeśli przełączanie wątków nie nastąpi tak, jak to opisałem, ale po prostu dlatego, że dwie różne buforowane kopie licznika były obecne w dwóch różnych rdzeniach procesora, a wątki działały na osobnym rdzeniu. Jeśli o to chodzi, jeden wątek może mieć licznik na jednej wartości, a drugi może mieć licznik na zupełnie innej wartości tylko z powodu buforowania.
W tym przykładzie ważne jest to, że licznik zmiennych został odczytany z pamięci głównej do pamięci podręcznej, zaktualizowany w pamięci podręcznej i zapisany z powrotem do pamięci głównej tylko w pewnym nieokreślonym punkcie później, kiedy pojawiła się bariera pamięci lub gdy pamięć podręczna była potrzebna na coś innego. Wykonanie licznika volatile
jest niewystarczające dla bezpieczeństwa wątków tego kodu, ponieważ test dla maksimum i przypisania są operacjami dyskretnymi, w tym przyrostem, który jest zestawem nieatomowych read+increment+write
instrukcji maszynowych, na przykład:
MOV EAX,counter
INC EAX
MOV counter,EAX
Zmienne zmienne są użyteczne tylko wtedy, gdy wszystkie wykonywane na nich operacje mają charakter „atomowy”, na przykład w moim przykładzie, w którym odwołanie do w pełni ukształtowanego obiektu jest odczytywane lub zapisywane (i faktycznie zazwyczaj jest zapisywane tylko z jednego punktu). Innym przykładem może być ulotna referencja tablicowa, na której opiera się lista kopiowania przy zapisie, pod warunkiem, że tablica została odczytana tylko przez pobranie lokalnej kopii referencji do niej.