Jednym z wielu problemów obok synchronizacji, jest widoczność zmiennych klasowych dla poszczególnych wątków. Nie jest to problem oczywisty, dlatego rozważę wpierw poniższy kod.
public class VolatileVariable { private static boolean done = false; public static void main(String[] args) { Runnable starting = () -> { for (int i = 0; i < 1000; i++) { System.out.println("Counter: " + i); } done = true; }; Runnable ending = () -> { int i = 1; while(!done) i++; System.out.println("Last item: " + i); }; Executor executor = Executors.newCachedThreadPool(); executor.execute(starting); executor.execute(ending); } }
Na konsoli powinno wyświetlić się:
Counter: 1 Counter: 2 ... Counter: 998 Counter: 999
Wynik nie jest poprawny, zmiana flagi done, nigdy nie została odczytana. Dzieje się tak dlatego, ponieważ słowo kluczowe static pozwala komputerowi na trzymanie tej wartości w pamięci podręcznej. Z punktu widzenia kompilatora, jest to wygodne rozwiązanie, pamięć komputera jest ograniczona, więc w ten sposób JVM może przyśpieszać pracę na odpowiednich wartościach. Problem zaczyna się pojawiać w środowisku wielowątkowym, gdy kilka wątków pracuje na tej samej zmiennej statycznej. Jeśli komputer zawiera więcej niż jeden procesor, każdy wątek może działać na innym procesorze. Ponieważ każdy procesor ma swoją pamięć podręczną, wartość zmiennej dla każdego z nich może być różna. Dodatkowo procesor ma możliwość stosować techniki optymalizacyjne, zmieniając kolejność wykonywanych rozkazów (oczywiście jeśli nie wpływa to na wynik końcowy obliczeń).
Zmienna volatile
Aby poprawić powyższy kod, wystarczy z korzystać ze specjalnego modyfikatora volatile. Nakładasz go na zmienną klasową, która jest współdzielona przez więcej niż jeden wątek. Skorzystanie z tego modyfikatora, gwarantuje widoczność rzeczywistej wartości pola dla wszystkich wątków Javy.
private static volatile boolean done = false;
Dzięki tej zmianie drugi wątek zostanie odblokowany:
... Counter: 996 Counter: 997 Counter: 998 Counter: 999 Last item: 19063057
Problemy przy użyciu volatile
Niestety udostępnianie rzeczywistej wartości zmiennej dla każdego wątku może doprowadzić do popularnego problemu w zakresie wielowątkowości, o nazwie wyścig. Dzieje się tak dlatego, że volatile nie jest alternatywą dla synchronizacji wątków. Jeśli potrzebujesz pracować na tej samej sekcji krytycznej, prawdopodobnie nie unikniesz potrzeby synchronizacji. Jedyną sensowną sytuacją kiedy warto rozważyć volatile jest użycie go na zmiennej, na której wykonywana jest operacja atomowa.
Widoczność zmiennych w wątkach
Istnieją też inne sposoby, aby zapewnić widoczność zmiennej:
- skorzystanie z modyfikatora final
- użycie blokad
- początkowa wartość zmiennej statycznej będzie widoczny przy skorzystaniu z inicjalizacji statycznej
- skorzystanie ze zmiennych atomowych
Kod do lekcji: https://github.com/developeronthego/java-advanced/tree/master/src/main/java/advanced/lesson14