Czy wątek! = Check jest bezpieczny?


140

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łą falsei wtedy wykonywanie programu przez długi czas nie generuje ani jednego truewyjś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

2
Co w tym kontekście oznacza „bezpieczne wątkowo”? Pytasz, czy zawsze zwróci fałsz?
JB Nizet

@JBNizet yes. Właśnie o tym myślałem.
Narendra Pathai

5
Nie zawsze nawet zwraca fałsz w kontekście jednowątkowym. To może być NaN ...
harold

4
Prawdopodobne wyjaśnienie: kod został skompilowany dokładnie na czas, a skompilowany kod ładuje zmienną tylko raz. To jest oczekiwane.
Marko Topolnik

3
Drukowanie indywidualnych wyników to kiepski sposób na testowanie wyścigów. Drukowanie (zarówno formatowanie, jak i zapisywanie wyników) jest stosunkowo kosztowne w porównaniu z testem (i czasami program kończy się blokowaniem zapisu, gdy przepustowość połączenia z terminalem lub sam terminal jest wolna). Ponadto, IO często zawiera muteksy własnych, które będą permutacji kolejność wykonywania swoich wątków (uwaga twoje indywidualne linie 1234:truenigdy 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).
Ben Jackson,

Odpowiedzi:


124

W przypadku braku synchronizacji ten kod

Object a;

public boolean test() {
    return a != a;
}

może produkować true. To jest kod bajtowy dlatest()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

jak widzimy adwukrotnie ładuje pole do lokalnych zmiennych, jest to operacja nieatomowa, jeśli azostała zmieniona w międzyczasie przez inny wątek, może dać porównanie false.

Również tutaj istotny jest problem z widocznością pamięci, nie ma gwarancji, że zmiany awprowadzone przez inny wątek będą widoczne dla bieżącego wątku.


22
Chociaż są to mocne dowody, kod bajtowy w rzeczywistości nie jest dowodem. To też musi być gdzieś w JLS ...
Marko Topolnik,

10
@Marko Zgadzam się z Twoim zdaniem, ale niekoniecznie z wnioskiem. Dla mnie powyższy kod bajtowy jest oczywistym / kanonicznym sposobem implementacji !=, 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 oznacza adwukrotne ładowanie .
Andrzej Doyle

20
Właściwie zakładając, że wygenerowany kod bajtowy jest zgodny z JLS, jest to dowód!
proskor

6
@Adrian: Po pierwsze: nawet jeśli to założenie jest niepoprawne, istnienie pojedynczego kompilatora, w którym można ocenić jako „prawdziwy”, wystarczy, aby wykazać, że czasami można go ocenić jako „prawdziwy” (nawet jeśli specyfikacja tego zabrania - co jest nie). Po drugie: Java jest dobrze określona i większość kompilatorów jest z nią ściśle zgodna. Warto je wykorzystać jako punkt odniesienia w tym zakresie. Po trzecie: używasz terminu „JRE”, ale nie sądzę, że oznacza to, co myślisz, że oznacza. . .
ruakh

2
@AlexanderTorstling - „Nie jestem pewien, czy to wystarczy, aby wykluczyć optymalizację pojedynczego odczytu”. Nie jest wystarczające. W rzeczywistości przy braku synchronizacji (i dodatkowych relacji „dzieje się przed”, które narzuca) optymalizacja jest ważna,
Stephen C

47

Czy czek jest a != abezpieczny dla wątków?

Jeśli apotencjalnie może zostać zaktualizowany przez inny wątek (bez odpowiedniej synchronizacji!), To nie.

Próbowałem to zaprogramować i używać wielu wątków, ale się nie udało. Chyba nie mogłem zasymulować wyścigu na moim komputerze.

To nic nie znaczy! Problem polega na tym, że jeśli wykonanie, w którym ajest 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.

Czy to oznacza, że ​​a! = A może powrócić true.

Tak, teoretycznie, w pewnych okolicznościach.

Alternatywnie, a != amógł wrócić, falsemimo że azmieniał się jednocześnie.


Odnośnie „dziwnego zachowania”:

Ponieważ mój program uruchamia się między niektórymi iteracjami, otrzymuję wartość flagi wyjściowej, co oznacza, że ​​odwołanie! = Check nie działa w tym samym odwołaniu. ALE po kilku iteracjach dane wyjściowe przyjmują stałą wartość false, a następnie wykonywanie programu przez długi czas nie generuje ani jednego prawdziwego wyniku.

To „dziwne” zachowanie jest zgodne z następującym scenariuszem wykonywania:

  1. 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.

  2. 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 ...)

  3. 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:

Wygląda na to, że programiści C lub C ++ nazywają „niezdefiniowanym zachowaniem” (zależnym od implementacji). Wygląda na to, że w javie może być kilka UB w narożnych przypadkach, takich jak ten.

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:

  • gdy są wykonywane przez jeden wątek, i
  • podczas wykonywania przez różne wątki, które synchronizują się poprawnie (zgodnie z modelem pamięci).

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).


Czy to oznacza, że a != amoże zwrócić prawdę?
proskor

Chodziło mi o to, że może na moim komputerze nie mogłem zasymulować, że powyższy kod nie jest bezpieczny dla wątków. Może więc kryje się za tym rozumowanie teoretyczne.
Narendra Pathai

@NarendraPathai - Nie ma teoretycznego powodu, dla którego nie możesz tego zademonstrować. Prawdopodobnie jest praktyczny powód ... a może po prostu nie miałeś szczęścia.
Stephen C

Sprawdź moją zaktualizowaną odpowiedź w programie, którego używam. Sprawdzenie czasami zwraca prawdę, ale dane wyjściowe wydają się dziwne.
Narendra Pathai

1
@NarendraPathai - Zobacz moje wyjaśnienie.
Stephen C

27

Udowodniono testem:

public class MyTest {

  private static Integer count=1;

  @Test(threadPoolSize = 1000, invocationCount=10000)
  public void test(){
    count = new Integer(new Random().nextInt());
    Assert.assertFalse(count != count);
  }

}

Mam 2 niepowodzenia na 10 000 wywołań. Więc NIE , to NIE jest bezpieczne wątkowo


6
Nawet nie sprawdzasz równości ... ta Random.nextInt()część jest zbyteczna. Mogłeś new Object()równie dobrze przetestować .
Marko Topolnik

@MarkoTopolnik Proszę sprawdzić moją zaktualizowaną odpowiedź w programie, którego używam. Sprawdzenie czasami zwraca prawdę, ale dane wyjściowe wydają się dziwne.
Narendra Pathai

1
Na marginesie, obiekty losowe są zwykle przeznaczone do ponownego wykorzystania, a nie tworzone za każdym razem, gdy potrzebujesz nowego int.
Simon Forsberg

15

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:

  1. Przeczytaj „a” dwa razy, umieść każdy na stosie, a następnie porównaj wyniki
  2. Przeczytaj „a” tylko raz, umieść go na stosie, zduplikuj (instrukcja „dup”) i uruchom porównanie
  3. Całkowicie usuń wyrażenie i zastąp je 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. AtomicIntegerJak wspomniał Juned Ahasan).

Aby uzyskać szczegółowe informacje na temat bezpieczeństwa wątków, przeczytaj JSR 133 ( model pamięci Java ).


Zadeklarowanie aas volatilenadal oznaczałoby dwa odrębne odczyty, z możliwością zmiany pomiędzy nimi.
Holger

6

Wszystko to dobrze wyjaśnił Stephen C. Dla zabawy możesz spróbować uruchomić ten sam kod z następującymi parametrami JVM:

-XX:InlineSmallCode=0

Powinno to zapobiec optymalizacji wykonanej przez JIT (robi na serwerze hotspot 7) i zobaczysz na truezawsze (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 != aa wiersz 31 to nawias zamykający while(true)).

  # {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
  0x00000000027dcc80: int3   
  0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
  0x00000000027dcc8c: data32 data32 xchg ax,ax
  0x00000000027dcc90: mov    DWORD PTR [rsp-0x6000],eax
  0x00000000027dcc97: push   rbp
  0x00000000027dcc98: sub    rsp,0x40
  0x00000000027dcc9c: mov    rbx,QWORD PTR [rdx+0x8]
  0x00000000027dcca0: mov    rbp,QWORD PTR [rdx+0x18]
  0x00000000027dcca4: mov    rcx,rdx
  0x00000000027dcca7: movabs r10,0x6e1a7680
  0x00000000027dccb1: call   r10
  0x00000000027dccb4: test   rbp,rbp
  0x00000000027dccb7: je     0x00000000027dccdd
  0x00000000027dccb9: mov    r10d,DWORD PTR [rbp+0x8]
  0x00000000027dccbd: cmp    r10d,0xefc158f4    ;   {oop('javaapplication27/TestThreadSafety$1')}
  0x00000000027dccc4: jne    0x00000000027dccf1
  0x00000000027dccc6: test   rbp,rbp
  0x00000000027dccc9: je     0x00000000027dcce1
  0x00000000027dcccb: cmp    r12d,DWORD PTR [rbp+0xc]
  0x00000000027dcccf: je     0x00000000027dcce1  ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
                                                ;   {poll}
  0x00000000027dccdb: jmp    0x00000000027dccd1
  0x00000000027dccdd: xor    ebp,ebp
  0x00000000027dccdf: jmp    0x00000000027dccc6
  0x00000000027dcce1: mov    edx,0xffffff86
  0x00000000027dcce6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dcceb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=112}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dccf0: int3   
  0x00000000027dccf1: mov    edx,0xffffffad
  0x00000000027dccf6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dccfb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=128}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
  0x00000000027dcd01: int3   

1
To jest dobry przykład rodzaju kodu, który JVM faktycznie wyprodukuje, gdy masz nieskończoną pętlę i wszystko można mniej więcej wyciągnąć. Rzeczywista „pętla” to trzy instrukcje od 0x27dccd1do 0x27dccdf. W jmppętli jest bezwarunkowa (ponieważ pętla jest nieskończona). Jedyne pozostałe dwie instrukcje w pętli to add rbc, 0x1- która jest inkrementowana countOfIterations(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), .. .
BeeOnRope

... i dziwnie wyglądająca testinstrukcja, która w rzeczywistości jest dostępna tylko dla dostępu do pamięci (uwaga, która eaxnigdy 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.
BeeOnRope

Co więcej, maszyna JVM całkowicie usunęła instance. a != instance.aporównanie z pętli i wykonuje je tylko raz, zanim pętla zostanie wprowadzona! Wie, że nie jest wymagane ponowne ładowanie instancelub aponieważ 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.
BeeOnRope

5

Nie, a != anie jest bezpieczny dla wątków. To wyrażenie składa się z trzech części: załaduj a, załaduj aponownie i wykonaj !=. Możliwe jest, że inny wątek uzyska wewnętrzną blokadę arodzica i zmieni wartość apomiędzy dwoma operacjami ładowania.

Innym czynnikiem jest to, czy ajest lokalny. Jeśli ajest lokalny, żadne inne wątki nie powinny mieć do niego dostępu i dlatego powinny być bezpieczne dla wątków.

void method () {
    int a = 0;
    System.out.println(a != a);
}

powinien również zawsze drukować false.

Zadeklarowanie ajako volatilenie rozwiązałoby problemu, jeśli ajest staticlub instancja. Problem nie polega na tym, że wątki mają różne wartości a, ale że jeden wątek ładuje się adwukrotnie z różnymi wartościami. To może rzeczywiście zrobić w przypadku mniej bezpieczny wątku .. Jeśli anie jest volatilenastępnie amogą być buforowane, a zmiana w innym wątku nie wpłynie na wartość pamięci podręcznej.


Twój przykład z synchronizedjest błędny: aby zagwarantować, że kod zostanie wydrukowany false, musiałyby tak być również wszystkie ustawione metody . asynchronized
ruakh

Dlaczego tak? Jeśli metoda jest zsynchronizowana, w jaki sposób każdy inny wątek uzyskałby wewnętrzną blokadę arodzica podczas wykonywania metody, niezbędną do ustawienia wartości a.
DoubleMx2

1
Twoje przesłanki są błędne. Możesz ustawić pole obiektu bez uzyskiwania jego wewnętrznej blokady. Java nie wymaga wątku, aby uzyskać wewnętrzną blokadę obiektu przed ustawieniem jego pól.
ruakh

3

Odnośnie dziwnego zachowania:

Ponieważ zmienna anie jest oznaczona jako volatile, w pewnym momencie amoże zostać zapisana w pamięci podręcznej przez wątek. Obie as od a != asą wówczas buforowane wersji, a więc zawsze taki sam (czyli flagjest teraz zawsze false).


0

Nawet proste czytanie nie jest atomowe. Jeśli ajest longi nie jest oznaczony jako volatileto, w 32-bitowych maszynach JVM long b = anie jest bezpieczny wątkowo .


lotność i atomowość nie są ze sobą powiązane. nawet jeśli
zaznaczę

Przypisanie lotnego, długiego pola jest zawsze atomowe. Inne operacje, takie jak ++, nie są.
ZhekaKozlov
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.