Czy zmienne statyczne są wspólne dla wątków?

95

Mój nauczyciel z wyższego poziomu Java na temat wątków powiedział coś, czego nie byłem pewien.

Stwierdził, że poniższy kod niekoniecznie zaktualizuje readyzmienną. Według niego, dwa wątki niekoniecznie współdzielą zmienną statyczną, szczególnie w przypadku, gdy każdy wątek (wątek główny versus ReaderThread) działa na swoim własnym procesorze i dlatego nie ma tych samych rejestrów / pamięci podręcznej / itp. I jednego procesora nie zaktualizuje drugiego.

Zasadniczo powiedział, że jest możliwe, że readyjest aktualizowany w głównym wątku, ale NIE w ReaderThread, więc toReaderThread będzie się zapętlać w nieskończoność.

Twierdził również, że program mógł wydrukować 0lub 42. Rozumiem, jak 42można wydrukować, ale nie 0. Wspomniał, że tak będzie w przypadkunumber zmiennej na wartość domyślną.

Pomyślałem, że być może nie ma gwarancji, że zmienna statyczna jest aktualizowana między wątkami, ale wydaje mi się to bardzo dziwne dla Javy. Czy tworzenie readyulotności rozwiązuje ten problem?

Pokazał ten kod:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}
dontocsata
źródło
Widoczność zmiennych nielokalnych nie zależy od tego, czy są to zmienne statyczne, pola obiektów czy elementy tablicy, wszystkie mają te same względy. (Z problemem polegającym na tym, że elementy tablicy nie mogą być zmienne.)
Paŭlo Ebermann,
1
zapytaj nauczyciela, jaką architekturę uważa, że ​​można zobaczyć „0”. Jednak teoretycznie ma rację.
bestsss
4
@bestsss Zadanie tego rodzaju pytania ujawniłoby nauczycielowi, że przeoczył cały sens tego, co mówił. Chodzi o to, że kompetentni programiści rozumieją, co jest gwarantowane, a co nie, i nie polegają na rzeczach, które nie są gwarantowane, przynajmniej nie bez dokładnego zrozumienia, co nie jest gwarantowane i dlaczego.
David Schwartz
Są współdzielone przez wszystko, co ładuje ten sam program ładujący klasy. W tym wątki.
Markiz Lorne
Twój nauczyciel (i przyjęta odpowiedź) ma w 100% rację, ale wspomnę, że rzadko się to zdarza - to jest problem, który będzie się ukrywał przez lata i ujawniał się tylko wtedy, gdy byłby najbardziej szkodliwy. Nawet krótkie testy próbujące ujawnić problem wydają się działać tak, jakby wszystko było w porządku (prawdopodobnie dlatego, że nie mają czasu, aby JVM przeprowadziła wiele optymalizacji), więc jest to naprawdę dobry problem, o którym należy pamiętać.
Bill K

Odpowiedzi:

75

Nie ma nic specjalnego w zmiennych statycznych, jeśli chodzi o widoczność. Jeśli są dostępne, każdy wątek może się do nich dostać, więc bardziej prawdopodobne jest, że wystąpią problemy ze współbieżnością, ponieważ są one bardziej narażone.

Istnieje problem z widocznością narzucony przez model pamięci maszyny JVM. Oto artykuł omawiający model pamięci i sposób, w jaki zapisy stają się widoczne dla wątków . Nie możesz liczyć na zmiany, które jeden wątek sprawi, że staną się widoczne dla innych wątków w odpowiednim czasie (w rzeczywistości JVM nie ma obowiązku, aby te zmiany były widoczne w ogóle dla Ciebie, w jakimkolwiek czasie), chyba że ustalisz relację zdarzenie przed .

Oto cytat z tego linku (podany w komentarzu Jed Wesley-Smith):

Rozdział 17 specyfikacji języka Java definiuje relację dzieje się przed operacjami pamięciowymi, takimi jak odczyty i zapisy współużytkowanych zmiennych. Rezultaty zapisu przez jeden wątek są widoczne dla odczytu przez inny wątek tylko wtedy, gdy operacja zapisu nastąpi - przed operacją odczytu. Zsynchronizowane i nietrwałe konstrukcje, a także metody Thread.start () i Thread.join () mogą tworzyć relacje zdarzają się przed. W szczególności:

  • Każda akcja w wątku ma miejsce - przed każdą akcją w tym wątku, która pojawia się później w kolejności programu.

  • Odblokowanie (zsynchronizowany blok lub wyjście metody) monitora następuje przed każdą kolejną blokadą (zsynchronizowanym blokiem lub wprowadzeniem metody) tego samego monitora. A ponieważ relacja dzieje się przed jest przechodnią, wszystkie akcje wątku przed odblokowaniem mają miejsce przed wszystkimi akcjami po zablokowaniu dowolnego wątku na tym monitorze.

  • Zapis do pola nietrwałego następuje przed każdym kolejnym odczytem tego samego pola. Zapisy i odczyty pól nietrwałych mają podobny wpływ na spójność pamięci, jak wchodzenie i wychodzenie z monitorów, ale nie wiążą się z blokowaniem wzajemnego wykluczania.

  • Wywołanie rozpoczęcia wątku następuje przed jakąkolwiek akcją w uruchomionym wątku.

  • Wszystkie akcje w wątku mają miejsce, zanim jakikolwiek inny wątek pomyślnie powróci z połączenia w tym wątku.

Nathan Hughes
źródło
3
W praktyce „w odpowiednim czasie” i „zawsze” są synonimami. Byłoby bardzo możliwe, że powyższy kod nigdy się nie kończy.
TREE
4
To także pokazuje inny anty-wzór. Nie używaj zmiennego do ochrony więcej niż jednego elementu wspólnego stanu. Tutaj liczba i gotowe są dwoma stanami, a aby zaktualizować / odczytać je konsekwentnie, potrzebujesz rzeczywistej synchronizacji.
TREE
5
Część dotycząca ostatecznego stawania się widocznym jest błędna. Bez żadnej wyraźnej relacji zdarzenie-przedtem nie ma gwarancji, że jakikolwiek zapis kiedykolwiek zostanie wyświetlony przez inny wątek, ponieważ JIT ma pełne prawo wrzucić odczyt do rejestru, a wtedy nigdy nie zobaczysz żadnej aktualizacji. Każde ewentualne obciążenie to szczęście i nie należy na nim polegać.
Jed Wesley-Smith
2
„chyba że używasz słowa kluczowego volatile lub synchronizuj”. należy przeczytać „chyba, że ​​istnieje stosowna relacja między autorem a czytelnikiem” oraz ten link: download.oracle.com/javase/6/docs/api/java/util/concurrent/ ...
Jed Wesley-Smith
2
@bestsss dobrze zauważone. Niestety ThreadGroup jest zepsuty na wiele sposobów.
Jed Wesley-Smith
37

Mówił o widoczności i nie należy go brać zbyt dosłownie.

Zmienne statyczne są rzeczywiście współdzielone między wątkami, ale zmiany wprowadzone w jednym wątku mogą nie być natychmiast widoczne dla innego wątku, przez co wydaje się, że istnieją dwie kopie zmiennej.

Ten artykuł przedstawia pogląd, który jest zgodny z tym, jak przedstawił informacje:

Najpierw musisz trochę zrozumieć model pamięci Java. Przez lata trochę się starałem, aby krótko i dobrze to wyjaśnić. Na dzień dzisiejszy najlepszym sposobem, w jaki mogę to opisać, jest wyobrażenie sobie tego w ten sposób:

  • Każdy wątek w Javie odbywa się w oddzielnej przestrzeni pamięci (to ewidentnie nieprawda, więc proszę o cierpliwość).

  • Musisz użyć specjalnych mechanizmów, aby zagwarantować, że komunikacja zachodzi między tymi wątkami, tak jak w systemie przekazywania wiadomości.

  • Zapisy w pamięci, które mają miejsce w jednym wątku, mogą „przeciekać” i być widziane przez inny wątek, ale nie jest to w żaden sposób gwarantowane. Bez wyraźnej komunikacji nie możesz zagwarantować, które zapisy zostaną odczytane przez inne wątki, ani nawet w kolejności, w jakiej będą widoczne.

...

model gwintu

Ale znowu, jest to po prostu model myślowy do myślenia o wątkach i niestabilności, a nie dosłownie jak działa JVM.

Bert F.
źródło
12

Zasadniczo to prawda, ale w rzeczywistości problem jest bardziej złożony. Na widoczność udostępnianych danych mogą wpływać nie tylko pamięci podręczne procesora, ale także wykonywanie instrukcji poza kolejnością.

Dlatego Java definiuje model pamięci , który określa, w jakich okolicznościach wątki mogą widzieć spójny stan udostępnionych danych.

W Twoim przypadku dodanie volatilegwarantuje widoczność.

axtavt
źródło
8

Są oczywiście „współdzielone” w tym sensie, że oboje odnoszą się do tej samej zmiennej, ale niekoniecznie widzą wzajemne aktualizacje. Dotyczy to każdej zmiennej, nie tylko statycznej.

Teoretycznie zapisy wykonane przez inny wątek mogą wydawać się w innej kolejności, chyba że zadeklarowano zmienne volatilelub zapisy zostały jawnie zsynchronizowane.

biziclop
źródło
4

W ramach jednego modułu ładującego klasy pola statyczne są zawsze udostępniane. Aby jawnie określić zakres danych do wątków, warto użyć narzędzia takiego jak ThreadLocal.

Kirk Woll
źródło
2

Podczas inicjowania statycznej zmiennej typu pierwotnego java domyślnie przypisuje wartość zmiennym statycznym

public static int i ;

kiedy definiujesz zmienną w ten sposób, domyślną wartością jest i = 0; dlatego istnieje możliwość uzyskania wartości 0. wtedy główny wątek aktualizuje wartość boolean ready na true. ponieważ ready jest zmienną statyczną, główny wątek i drugi wątek odwołują się do tego samego adresu pamięci, więc zmienna ready zmienia się. więc dodatkowy wątek wychodzi z pętli while i wypisuje wartość. podczas drukowania wartość zainicjowana wartością number jest 0. jeśli proces wątku przeszedł pętlę while przed zmienną numer aktualizacji głównego wątku. wtedy istnieje możliwość wydrukowania 0

NuOne
źródło
-2

@dontocsata możesz wrócić do swojego nauczyciela i trochę go wyszkolić :)

kilka notatek z prawdziwego świata i niezależnie od tego, co widzisz lub co Ci powiedzą UWAGA, poniższe słowa odnoszą się do tego konkretnego przypadku w podanej kolejności.

Następujące dwie zmienne będą znajdować się w tej samej linii pamięci podręcznej w praktycznie każdej znanej architekturze.

private static boolean ready;  
private static int number;  

Thread.exit (wątek główny) gwarantuje wyjście i exit powoduje ograniczenie pamięci ze względu na usunięcie wątku grupy wątków (i wiele innych problemów). (jest to zsynchronizowane wywołanie i nie widzę jednego sposobu na zaimplementowanie bez części synchronizacyjnej, ponieważ ThreadGroup musi również zakończyć się, jeśli nie pozostały żadne wątki demonów itp.).

Uruchomiony wątek ReaderThreadbędzie podtrzymywał proces, ponieważ nie jest demonem! Tak więc readyi numberzostaną spłukane razem (lub numer przed, jeśli nastąpi zmiana kontekstu) i nie ma prawdziwego powodu do zmiany kolejności w tym przypadku, przynajmniej nie mogę nawet pomyśleć o jednym. Aby zobaczyć cokolwiek innego, będziesz potrzebować czegoś naprawdę dziwnego 42. Ponownie zakładam, że obie zmienne statyczne będą w tej samej linii pamięci podręcznej. Po prostu nie mogę sobie wyobrazić linii pamięci podręcznej o długości 4 bajtów LUB maszyny JVM, która nie przypisze ich w ciągłym obszarze (linia pamięci podręcznej).

bestsss
źródło
3
@bestsss Dziś to wszystko prawda, ale prawda opiera się na aktualnej implementacji JVM i architekturze sprzętowej, a nie na semantyce programu. Oznacza to, że program jest nadal uszkodzony, mimo że może działać. Łatwo jest znaleźć trywialny wariant tego przykładu, który w rzeczywistości zawodzi w podany sposób.
Jed Wesley-Smith
1
Powiedziałem, że nie jest to zgodne ze specyfikacją, jednak jako nauczyciel przynajmniej znajdź odpowiedni przykład, który może faktycznie zawieść w przypadku jakiejś architektury towarowej, więc przykład jest trochę prawdziwy.
bestsss
6
Prawdopodobnie najgorsza rada dotycząca pisania kodu bezpiecznego dla wątków, jaką widziałem.
Lawrence Dol
4
@Bestsss: Prosta odpowiedź na Twoje pytanie jest prosta: „Koduj zgodnie ze specyfikacją i dokumentem, a nie z efektami ubocznymi Twojego konkretnego systemu lub implementacji”. Jest to szczególnie ważne w przypadku platformy maszyny wirtualnej zaprojektowanej tak, aby była niezależna od podstawowego sprzętu.
Lawrence Dol,
1
@Bestsss: Nauczyciel zwraca uwagę na to, że (a) kod może dobrze działać, gdy go testujesz, oraz (b) kod jest uszkodzony, ponieważ zależy to od działania sprzętu, a nie od gwarancji specyfikacji. Chodzi o to, że wygląda na to, że jest OK, ale nie jest OK.
Lawrence Dol,