Wiem, że operacje złożone, takie jak, i++
nie są bezpieczne dla wątków, ponieważ obejmują wiele operacji.
Ale czy sprawdzanie referencji z samym sobą jest operacją bezpieczną dla wątków?
a != a //is this thread-safe
Próbowałem to zaprogramować i używać wielu wątków, ale to się nie udało. Chyba nie mogłem zasymulować wyścigu na mojej maszynie.
EDYTOWAĆ:
public class TestThreadSafety {
private Object a = new Object();
public static void main(String[] args) {
final TestThreadSafety instance = new TestThreadSafety();
Thread testingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
long countOfIterations = 0L;
while(true){
boolean flag = instance.a != instance.a;
if(flag)
System.out.println(countOfIterations + ":" + flag);
countOfIterations++;
}
}
});
Thread updatingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
instance.a = new Object();
}
}
});
testingReferenceThread.start();
updatingReferenceThread.start();
}
}
To jest program, którego używam do testowania bezpieczeństwa wątków.
Dziwne zachowanie
Ponieważ mój program uruchamia się między niektórymi iteracjami, otrzymuję wartość flagi wyjściowej, co oznacza, że !=
sprawdzenie odniesienia nie powiedzie się w tym samym odwołaniu. ALE po kilku iteracjach wyjście staje się wartością stałą false
i wtedy wykonywanie programu przez długi czas nie generuje ani jednego true
wyjścia.
Jak sugeruje wynik, po kilku n (nie ustalonych) iteracjach, wyjście wydaje się mieć stałą wartość i nie zmienia się.
Wynik:
W przypadku niektórych iteracji:
1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
źródło
1234:true
nigdy rozbić siebie ). Test wyścigowy wymaga ciaśniejszej pętli wewnętrznej. Wydrukuj podsumowanie na końcu (jak ktoś zrobił poniżej z frameworkiem testów jednostkowych).Odpowiedzi:
W przypadku braku synchronizacji ten kod
może produkować
true
. To jest kod bajtowy dlatest()
jak widzimy
a
dwukrotnie ładuje pole do lokalnych zmiennych, jest to operacja nieatomowa, jeślia
została zmieniona w międzyczasie przez inny wątek, może dać porównaniefalse
.Również tutaj istotny jest problem z widocznością pamięci, nie ma gwarancji, że zmiany
a
wprowadzone przez inny wątek będą widoczne dla bieżącego wątku.źródło
!=
, który obejmuje osobne ładowanie LHS i RHS. Jeśli więc JLS nie wspomina nic konkretnego na temat optymalizacji, gdy LHS i RHS są składniowo identyczne, zastosowanie miałaby ogólna reguła, co oznaczaa
dwukrotne ładowanie .Jeśli
a
potencjalnie może zostać zaktualizowany przez inny wątek (bez odpowiedniej synchronizacji!), To nie.To nic nie znaczy! Problem polega na tym, że jeśli wykonanie, w którym
a
jest aktualizowane przez inny wątek, jest dozwolone przez JLS, kod nie jest bezpieczny wątkowo. Fakt, że nie można spowodować wystąpienia sytuacji wyścigu z konkretnym przypadkiem testowym na określonej maszynie i konkretnej implementacji Javy, nie wyklucza, że wystąpi to w innych okolicznościach.Tak, teoretycznie, w pewnych okolicznościach.
Alternatywnie,
a != a
mógł wrócić,false
mimo żea
zmieniał się jednocześnie.Odnośnie „dziwnego zachowania”:
To „dziwne” zachowanie jest zgodne z następującym scenariuszem wykonywania:
Program jest ładowany i maszyna JVM rozpoczyna interpretację kodów bajtowych. Ponieważ (jak widzieliśmy z danych wyjściowych javap) kod bajtowy wykonuje dwa obciążenia, (najwyraźniej) czasami widzisz wyniki stanu wyścigu.
Po pewnym czasie kod jest kompilowany przez kompilator JIT. Optymalizator JIT zauważa, że istnieją dwa obciążenia tego samego gniazda pamięci (
a
) blisko siebie i optymalizuje drugi z dala. (W rzeczywistości istnieje szansa, że całkowicie zoptymalizuje test ...)Teraz stan wyścigu już się nie objawia, ponieważ nie ma już dwóch ładunków.
Należy pamiętać, że jest to wszystko zgodne z tym, co pozwala JLS implementacja Javy zrobić.
@kriss skomentował w ten sposób:
Model pamięci Java (określony w JLS 17.4 ) określa zestaw warunków wstępnych, zgodnie z którymi jeden wątek ma pewność, że zobaczy wartości pamięci zapisane przez inny wątek. Jeśli jeden wątek próbuje odczytać zmienną zapisaną przez inny, a te warunki wstępne nie są spełnione, to może być wiele możliwych wykonań ... z których niektóre mogą być nieprawidłowe (z punktu widzenia wymagań aplikacji). Innymi słowy, zdefiniowano zbiór możliwych zachowań (tj. Zbiór „dobrze uformowanych wykonań”), ale nie możemy powiedzieć, które z tych zachowań wystąpią.
Kompilator może łączyć i zmieniać kolejność ładowań oraz zapisywać (i robić inne rzeczy) pod warunkiem, że efekt końcowy kodu jest taki sam:
Ale jeśli kod nie synchronizuje się poprawnie (a zatem relacje „zdarza się przed” nie ograniczają wystarczająco zestawu poprawnie sformułowanych wykonań), kompilator może zmienić kolejność ładowań i przechowywać w sposób, który dałby „niepoprawne” wyniki. (Ale to tak naprawdę tylko mówi, że program jest nieprawidłowy).
źródło
a != a
może zwrócić prawdę?Udowodniono testem:
Mam 2 niepowodzenia na 10 000 wywołań. Więc NIE , to NIE jest bezpieczne wątkowo
źródło
Random.nextInt()
część jest zbyteczna. Mogłeśnew Object()
równie dobrze przetestować .Nie, nie jest. W celu porównania maszyna wirtualna Java musi umieścić dwie wartości do porównania na stosie i uruchomić instrukcję porównania (która zależy od typu „a”).
Maszyna wirtualna Java może:
false
W pierwszym przypadku inny wątek mógłby zmodyfikować wartość „a” między dwoma odczytami.
Wybór strategii zależy od kompilatora Java i środowiska wykonawczego Java (zwłaszcza kompilatora JIT). Może się nawet zmienić w trakcie działania programu.
Jeśli chcesz mieć pewność, w jaki sposób uzyskujesz dostęp do zmiennej, musisz to zrobić
volatile
(tak zwana „połowa bariery pamięci”) lub dodać pełną barierę pamięci (synchronized
). Możesz także użyć jakiegoś wyższego poziomu API (np.AtomicInteger
Jak wspomniał Juned Ahasan).Aby uzyskać szczegółowe informacje na temat bezpieczeństwa wątków, przeczytaj JSR 133 ( model pamięci Java ).
źródło
a
asvolatile
nadal oznaczałoby dwa odrębne odczyty, z możliwością zmiany pomiędzy nimi.Wszystko to dobrze wyjaśnił Stephen C. Dla zabawy możesz spróbować uruchomić ten sam kod z następującymi parametrami JVM:
Powinno to zapobiec optymalizacji wykonanej przez JIT (robi na serwerze hotspot 7) i zobaczysz na
true
zawsze (zatrzymałem się na 2 000 000, ale przypuszczam, że po tym będzie kontynuowany).Dla informacji poniżej znajduje się kod JIT. Szczerze mówiąc, nie czytam montażu na tyle płynnie, aby wiedzieć, czy test został faktycznie wykonany lub skąd pochodzą dwa obciążenia. (wiersz 26 to test,
flag = a != a
a wiersz 31 to nawias zamykającywhile(true)
).źródło
0x27dccd1
do0x27dccdf
. Wjmp
pętli jest bezwarunkowa (ponieważ pętla jest nieskończona). Jedyne pozostałe dwie instrukcje w pętli toadd rbc, 0x1
- która jest inkrementowanacountOfIterations
(mimo że pętla nigdy nie zostanie zakończona, więc ta wartość nie zostanie odczytana: być może jest to potrzebne w przypadku włamania się do niej w debugerze), .. .test
instrukcja, która w rzeczywistości jest dostępna tylko dla dostępu do pamięci (uwaga, któraeax
nigdy nie jest nawet ustawiona w metodzie!): jest to specjalna strona, która jest ustawiona jako nieczytelna, gdy JVM chce wyzwolić wszystkie wątki aby osiągnąć bezpieczny punkt, więc może wykonać gc lub inną operację, która wymaga, aby wszystkie wątki były w znanym stanie.instance. a != instance.a
porównanie z pętli i wykonuje je tylko raz, zanim pętla zostanie wprowadzona! Wie, że nie jest wymagane ponowne ładowanieinstance
luba
ponieważ nie są zadeklarowane jako ulotne i nie ma innego kodu, który mógłby je zmienić w tym samym wątku, więc zakłada, że są takie same w całej pętli, na co pozwala pamięć Model.Nie,
a != a
nie jest bezpieczny dla wątków. To wyrażenie składa się z trzech części: załaduja
, załaduja
ponownie i wykonaj!=
. Możliwe jest, że inny wątek uzyska wewnętrzną blokadęa
rodzica i zmieni wartośća
pomiędzy dwoma operacjami ładowania.Innym czynnikiem jest to, czy
a
jest lokalny. Jeślia
jest lokalny, żadne inne wątki nie powinny mieć do niego dostępu i dlatego powinny być bezpieczne dla wątków.powinien również zawsze drukować
false
.Zadeklarowanie
a
jakovolatile
nie rozwiązałoby problemu, jeślia
jeststatic
lub instancja. Problem nie polega na tym, że wątki mają różne wartościa
, ale że jeden wątek ładuje sięa
dwukrotnie z różnymi wartościami. To może rzeczywiście zrobić w przypadku mniej bezpieczny wątku .. Jeślia
nie jestvolatile
następniea
mogą być buforowane, a zmiana w innym wątku nie wpłynie na wartość pamięci podręcznej.źródło
synchronized
jest błędny: aby zagwarantować, że kod zostanie wydrukowanyfalse
, musiałyby tak być również wszystkie ustawione metody .a
synchronized
a
rodzica podczas wykonywania metody, niezbędną do ustawienia wartościa
.Odnośnie dziwnego zachowania:
Ponieważ zmienna
a
nie jest oznaczona jakovolatile
, w pewnym momenciea
może zostać zapisana w pamięci podręcznej przez wątek. Obiea
s oda != a
są wówczas buforowane wersji, a więc zawsze taki sam (czyliflag
jest teraz zawszefalse
).źródło
Nawet proste czytanie nie jest atomowe. Jeśli
a
jestlong
i nie jest oznaczony jakovolatile
to, w 32-bitowych maszynach JVMlong b = a
nie jest bezpieczny wątkowo .źródło