Czuła operacja w moim laboratorium dzisiaj poszła całkowicie nie tak. Siłownik mikroskopu elektronowego przekroczył granicę, a po łańcuchu wydarzeń straciłem sprzęt o wartości 12 milionów dolarów. Zawęziłem ponad 40 000 linii w wadliwym module do tego:
import java.util.*;
class A {
static Point currentPos = new Point(1,2);
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
new Thread() {
void f(Point p) {
synchronized(this) {}
if (p.x+1 != p.y) {
System.out.println(p.x+" "+p.y);
System.exit(1);
}
}
@Override
public void run() {
while (currentPos == null);
while (true)
f(currentPos);
}
}.start();
while (true)
currentPos = new Point(currentPos.x+1, currentPos.y+1);
}
}
Niektóre próbki danych wyjściowych, które otrzymuję:
$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651
Ponieważ nie ma tutaj żadnej arytmetyki zmiennoprzecinkowej i wszyscy wiemy, że liczby całkowite ze znakiem zachowują się dobrze w przypadku przepełnienia w Javie, sądzę, że nie ma nic złego w tym kodzie. Jednak pomimo danych wyjściowych wskazujących, że program nie osiągnął warunku wyjścia, osiągnął warunek wyjścia (został osiągnięty i nie został osiągnięty?). Czemu?
Zauważyłem, że tak się nie dzieje w niektórych środowiskach. Korzystam z OpenJDK 6 na 64-bitowym systemie Linux.
final
kwalifikatora (który nie ma wpływu na wygenerowany kod bajtowy) do pólx
iy
„rozwiązuje” błąd. Chociaż nie wpływa to na kod bajtowy, pola są z nim oznaczone, co prowadzi mnie do wniosku, że jest to efekt uboczny optymalizacji JVM.Point
p
Konstrukcja A jest spełniającap.x+1 == p.y
, a następnie do wątku odpytywania jest przekazywane odwołanie . W końcu wątek odpytywania decyduje się wyjść, ponieważ uważa, że warunek nie jest spełniony dla jednego zPoint
otrzymywanych komunikatów, ale następnie dane wyjściowe konsoli wskazują, że powinien był zostać spełniony. Brakvolatile
tutaj oznacza po prostu, że wątek wyborczy może utknąć, ale najwyraźniej nie jest to problemem.synchronized
powoduje, że błąd się nie zdarza? To dlatego, że musiałem losowo pisać kod, dopóki nie znalazłem takiego, który odtworzyłby to zachowanie deterministycznie.Odpowiedzi:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
robi kilka rzeczy, w tym zapisuje wartości domyślne dox
iy
(0), a następnie zapisuje swoje wartości początkowe w konstruktorze. Ponieważ obiekt nie został bezpiecznie opublikowany, te 4 operacje zapisu mogą być dowolnie zmieniane przez kompilator / JVM.Tak więc z punktu widzenia wątku czytającego jest legalnym wykonaniem, aby czytać
x
z nową wartością, aley
na przykład z domyślną wartością 0. Zanim dotrzesz doprintln
instrukcji (która, nawiasem mówiąc, jest zsynchronizowana i dlatego wpływa na operacje odczytu), zmienne mają swoje wartości początkowe, a program wypisuje wartości oczekiwane.Oznaczenie
currentPos
jakovolatile
zapewni bezpieczną publikację, ponieważ Twój obiekt jest faktycznie niezmienny - jeśli w twoim rzeczywistym przypadku obiekt zostanie zmutowany po budowie,volatile
gwarancje nie będą wystarczające i ponownie zobaczysz niespójny obiekt.Alternatywnie możesz zrobić
Point
niezmienne, które zapewni również bezpieczną publikację, nawet bez użyciavolatile
. Aby osiągnąć niezmienność, wystarczy zaznaczyćx
iy
zakończyć.Jako dodatkowa uwaga i jak już wspomniano,
synchronized(this) {}
JVM może potraktować ją jako zakaz operacji (rozumiem, że uwzględniłeś ją w celu odtworzenia zachowania).źródło
Ponieważ
currentPos
jest zmieniany poza wątkiem, należy go oznaczyć jakovolatile
:Bez niestabilności nie ma gwarancji, że wątek będzie czytał aktualizacje programu currentPos, które są tworzone w głównym wątku. Tak więc nowe wartości są nadal zapisywane dla currentPos, ale wątek nadal używa poprzednich wersji buforowanych ze względu na wydajność. Ponieważ tylko jeden wątek modyfikuje currentPos, możesz uciec bez blokad, co poprawi wydajność.
Wyniki wyglądają znacznie inaczej, jeśli odczytasz wartości tylko raz w wątku, aby użyć ich w porównaniu i późniejszym ich wyświetleniu. Kiedy to robię, poniższe
x
zawsze są wyświetlane jako1
iy
różnią się między0
pewną dużą liczbą całkowitą. Myślę, że jego zachowanie w tym momencie jest nieco niezdefiniowane bezvolatile
słowa kluczowego i możliwe jest, że kompilacja kodu JIT przyczynia się do tego, że działa on w ten sposób. Również jeśli skomentuję pustysynchronized(this) {}
blok, kod również działa i podejrzewam, że dzieje się tak, ponieważ blokowanie powoduje wystarczające opóźnienie,currentPos
a jego pola są ponownie odczytywane, a nie używane z pamięci podręcznej.źródło
volatile
.Masz zwykłą pamięć, odniesienie „currentpos” oraz obiekt Point i jego pola za nim, współdzielone między 2 wątkami, bez synchronizacji. Zatem nie ma zdefiniowanego porządku między zapisem, który ma miejsce w tej pamięci w głównym wątku, a odczytami w utworzonym wątku (nazwij go T).
Główny wątek wykonuje następujące zapisy (zignorowanie początkowej konfiguracji punktu, spowoduje, że px i py będą miały wartości domyślne):
Ponieważ w tych zapisach nie ma nic specjalnego pod względem synchronizacji / barier, środowisko wykonawcze może dowolnie pozwolić, aby wątek T zobaczył, że występują one w dowolnej kolejności (główny wątek oczywiście zawsze widzi zapisy i odczyty uporządkowane zgodnie z kolejnością programów) i występują w dowolnym punkcie między odczytami w T.
Więc T robi:
Biorąc pod uwagę, że nie ma powiązań porządkowych między zapisami w main, a odczytami w T, istnieje oczywiście kilka sposobów, dzięki którym można uzyskać wynik, ponieważ T może zobaczyć zapis main w currentpos przed zapisami w currentpos.y lub currentpos.x:
i tak dalej ... Jest tu wiele wyścigów danych.
Podejrzewam, że błędne założenie polega na tym, że zapisy, które wynikają z tego wiersza, są widoczne we wszystkich wątkach w kolejności programowej wykonywanego wątku:
Java nie daje takiej gwarancji (byłoby to fatalne z punktu widzenia wydajności). Coś więcej trzeba dodać, jeśli twój program potrzebuje gwarantowanej kolejności zapisów w stosunku do odczytów w innych wątkach. Inni sugerowali, aby pola x, y były ostateczne, lub alternatywnie uczynić prądy prądu zmiennymi.
Korzystanie z parametru final ma tę zaletę, że sprawia, że pola są niezmienne, a zatem umożliwia buforowanie wartości. Korzystanie z niestabilnych prowadzi do synchronizacji przy każdym zapisie i odczycie prądów prądowych, co może zaszkodzić wydajności.
Szczegółowe informacje na ten temat można znaleźć w rozdziale 17 specyfikacji języka Java: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
(Początkowa odpowiedź zakładała słabszy model pamięci, ponieważ nie byłem pewien, czy gwarantowana zmienność JLS jest wystarczająca. Odpowiedź edytowana w celu odzwierciedlenia komentarza od assylias, wskazując, że model Java jest silniejszy - zdarza się, zanim jest przechodni - i tak niestabilny na prądach również wystarcza ).
źródło
currentPos
zostanie zmienione, przypisanie zapewnia bezpieczną publikacjęcurrentPos
obiektu, a także jego członków, nawet jeśli same nie są niestabilne.Point currentPos = new Point(x, y)
masz 3 zapisy: (w1)this.x = x
, (w2)this.y = y
i (w3)currentPos = the new point
. Kolejność programów gwarantuje, że hb (w1, w3) i hb (w2, w3). Później w programie, który czytasz (r1)currentPos
. JeślicurrentPos
nie jest lotny, nie ma hb między r1 a w1, w2, w3, więc r1 mógłby zaobserwować dowolną (lub żadną) z nich. Dzięki lotnym wprowadzasz hb (w3, r1). I relacja hb jest przechodnia, więc wprowadzasz także hb (w1, r1) i hb (w2, r1). Jest to podsumowane w praktyce Java Concurrency w praktyce (3.5.3. Bezpieczne idiomy publikacji).Możesz użyć obiektu do synchronizacji zapisów i odczytów. W przeciwnym razie, jak powiedziano wcześniej, zapis do currentPos nastąpi w środku dwóch odczytów p.x + 1 i py
źródło
sem
nie jest udostępniony, i traktować zsynchronizowaną instrukcję jako brak możliwości ... Fakt, że rozwiązuje problem, jest czystym szczęściem.Uzyskujesz dostęp do currentPos dwa razy i nie dajesz żadnej gwarancji, że nie zostanie on zaktualizowany pomiędzy tymi dwoma dostępami.
Na przykład:
Zasadniczo porównujesz dwa różne punkty.
Zwróć uwagę, że nawet zmienność currentPos nie ochroni cię przed tym, ponieważ są to dwa osobne odczyty dla wątku roboczego.
Dodaj
metoda do twojej klasy punktów. Zapewni to, że podczas sprawdzania x + 1 == y zostanie użyta tylko jedna wartość currentPos.
źródło