Uchwycić volatile. Czyli o zmiennych ulotnych w Javie słów kilka

sages.pl 3 lat temu
Zmienna "volatile" jest jednym ze słów kluczowych w języku Java. Znaczenie tego słowa często jest błędnie rozumiane choćby ze względu na fakt, iż występuje ono w języku C/C++ i jego przeznaczenie jest zgoła inne niż w Javie. Być może dotychczas go nie używałeś bądź choćby nie spotkałeś się z nim w swojej codziennej pracy z kodem.
Przyczyny takiego stanu rzeczy można dopatrywać się w tym, iż da się z powodzeniem pisać poprawne programy nie wiedząc choćby o istnieniu tej konstrukcji. W związku z tym *volatile* może być uznane w pewnym sensie za nadmiarowe. Inaczej mówiąc, dowolny program (również współbieżny - w zasadzie tylko w takich programach sensownym jest rozważanie *volatile*) może zostać skonstruowany poprawnie nie używając w ogóle tego słowa. Bez najmniejszego kłopotu możemy zastąpić zmienne ulotne (czyli zmienne *volatile*) przy pomocy bloku synchronized i na tym oprzeć poprawność programu. Relacja w drugą stronę jednak nie zachodzi, tj. nie da się zastąpić dowolnego programu opartego o synchronized zmienną *volatile*. Z tego właśnie powodu warto spojrzeć na tą konstrukcję nie tylko pod kątem jej znaczenia, ale też jej zastosowań.

### Operacje na zmiennych volatile

Zmienna *volatile* w gruncie rzeczy nie różni się od swojej "normalnej" wersji. Różnica pojawia się przy operacjach zapisu i odczytu na takich zmiennych. Wyobraźmy sobie na moment, iż mamy zadeklarowaną zmienną v:

```
volatile [Type] v;
```

Specyfikacja języka Java gwarantuje, iż wątek odczytujący wartość zmiennej v widzi zawsze ostatni zapis do tej zmiennej, być może wykonany w innym wątku. Co więcej, wątek odczytujący wartość zmiennej v obserwuje wynik wszystkich zapisów, które zostały wykonane w innym wątku przed wykonaniem zapisu do zmiennej v.

![obraz1.webp](/uploads/obraz1_9fd067c415.webp)


Powyższy rysunek w sposób graficzny przedstawia to, co zostało wyżej opisane.

W dużym uproszczeniu można powiedzieć, iż *volatile* sygnalizuje kompilatorowi i maszynie wirtualnej, iż tak oznaczona zmienna może być współdzielona przez wątki. W związku z tym kompilator oraz runtime (JVM) powinny powstrzymywać się od wykonywania:
* zmiany kolejności wykonywanych operacji na pamięci,
* różnych optymalizacji, np. polegających na cache'owaniu wartości zmiennej.

W celu lepszego zrozumienia problematyki spójrzmy na następujący program:

```
boolean x = false;

T1: T2:
while(!x) {} x = true
```

Dla jasności przekazu programy są uproszczone i mają postać pseudokodu. Zawartość kolumny *T1* reprezentuje kod wykonywany przez wątek *T1* natomiast kod w drugiej kolumnie przedstawia kod wykonywany w innym wątku - *T2*.
Na pierwszy rzut oka może wydawać się, iż ten program musi się zakończyć. Nic bardziej mylnego. Może zdarzyć się tak, iż kompilator bądź JVM uzna, iż nie wykona zapisu do globalnej zmiennej x. Zarówno JVM jak i kompilator są uprawnieni do wykonania tego typu zabiegu ponieważ zezwala na to specyfikacja języka.
Tak napisany program jest po prostu niepoprawny. Wzbogacenie zmiennej x o atrybut volatile sprawia, iż taki kod jest już poprawny - mamy bowiem gwarancję, iż odczyt w wątku *T1* zaobserwuje zapis wykonany w wątku *T2*.

Spójrzmy teraz jeszcze na inny program:

```
int a = 0, b = 0;

T1: T2:
int r1 = b; a = 1;
int r2 = a; b = 1;
```

Załóżmy, iż wykonujemy ten program wiele razy i zapisujemy wyniki odczytów poczynionych w wątku *T1*. Po wykonaniu takiego testu możemy uzyskać następujące wyniki odczytów *r1*,*r2*:

```
[0,0; 1,1; 0,1, 1,0]
```

O ile trzy pierwsze rezultaty nie zaskakują i są w prosty sposób wytłumaczalne, o tyle wynik *1*,*0* wydaje się być niemożliwy do uzyskania. Przecież, skoro wątek *T1* zaobserwował zapis do zmiennej *b*, a ten zapis odbył się po zapisie do zmiennej *a* w wątku *T2*, to w takim razie odczyt zmiennej *a*, który odbywa się po odczycie *b* powinien zwrócić wartość 1. Wynik *1*,*0* przeczy jednak takiemu przebiegowi. Jedyne wyjaśnienie takiego wyniku prowadzi do konkluzji, iż została zmieniona kolejność operacji. Takie wykonanie jest zgodne z *JLS (Java Language Specification)*. Oznaczenie zmiennych *a*,*b* jako ulotnych gwarantuje, iż kolejność tych operacji nie zostanie zmieniona. W konsekwencji jedyne dopuszczalne wyniki to:

```
[0,0; 1,1; 0,1]
```

Wynika to wprost z semantyki odczytów i zapisów *volatile* - o ile wątek *T1* odczytał wartość *1* ze zmiennej *b* to niemożliwym jest, aby późniejszy odczyt zmiennej *a* w sensie porządku programu odczytał wartość *0* gdyż standard języka gwarantuje, iż o ile wątek *T1* odczytał wartość zmiennej *volatile* to zaobserwuje też wcześniejsze zapisy, a więc w szczególności zapis do zmiennej *a*.

Warto wspomnieć, iż volatile ma jeszcze jedno, dodatkowe znaczenie w przypadku zmiennych typu **long** i **double**. Generalnie, zapisy i odczyty do zmiennych reprezentujących typy proste i referencyjne są atomowe. Wyjątek stanowią tutaj wcześniej wspomniane **long** i **double**. Odczyty i zapisy do takich zmiennych nie mają gwarancji atomowości. Z pomocą przychodzi tutaj oznaczenie ich jako *volatile* - operacje odczytu i zapisu na takich zmiennych są atomowe.

### Kiedy warto użyć volatile?

Pierwszym powodem, dla którego warto byłoby sięgnąć po ten mechanizm, mogą być kwestie wydajnościowe. Nie w każdym przypadku, w którym wątki współdzielą zasób konieczne jest korzystanie z sekcji *synchronized*, a więc de facto blokowania (odblokowywania) monitora. Czasami wystarczą gwarancje, o których niżej, dostarczane przez zmienną *volatile*. Spójrzmy na poniższy prosty przykład:

```
public class CompareSynchronizedAndVolatileRead {
static volatile int v;
static int i;

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public static void takeLock(Blackhole blackhole) {
synchronized(CompareSynchronizedAndVolatileRead.class) {
blackhole.consume(i);
}
}


@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public static void volatileRead(Blackhole blackhole) {
blackhole.consume(v);
}

public static void main(String[] args) {
org.openjdk.jmh.Main.main(args);
}
}
```

Przedstawiony program wyznacza przy pomocy benchmarku *Java Microbenchmark Harness (JMH)* ile czasu zajmuje wykonanie tych dwóch funkcji. Obydwie metody realizują tą samą funkcjonalność - odczyt zmiennej całkowitoliczbowej w sposób poprawny pod kątem współbieżności, tzw. *threadsafe*. W pierwszym przypadku żeby to zrobić używamy bloku *synchronized*, w drugim zaś odczytu zmiennej typu *volatile*.
Na potrzeby naszych rozważań możemy pominąć wyrażenie *Blackhole.consume* - nie jest ono istotne. Wyniki prezentują się następująco:

Wynik jest oczywiście taki, jakiego należałoby się spodziewać. Dość powiedzieć, iż operacje na zmiennych *volatile* nie są blokujące, w przeciwieństwie do zajmowania monitora. Wynika to po prostu z wcześniej wspomnianego już faktu, iż operacje na zmiennej “ulotnej” same w sobie są lżejszym mechanizmem synchronizacji.

Drugim argumentem, który przemawiałby za *volatile* jest fakt, iż w pewnych przypadkach użycie go ułatwia implementację, a co za tym idzie ta jest prostsza w odbiorze. Generalnie, poza jakimiś szczególnymi przypadkami, to ten drugi argument powinien przeważać nad tym pierwszym. W tym miejscu należy jednak uważać gdyż nadużycie *volatile* może się w tym kontekście okazać bronią obosieczną.

![obraz2.webp](/uploads/obraz2_489b083700.webp)


Wynik jest oczywiście taki, jakiego należałoby się spodziewać. Dość powiedzieć, iż operacje na zmiennych *volatile* nie są blokujące, w przeciwieństwie do zajmowania monitora. Wynika to po prostu z wcześniej wspomnianego już faktu, iż operacje na zmiennej “ulotnej” same w sobie są lżejszym mechanizmem synchronizacji.

Drugim argumentem, który przemawiałby za *volatile* jest fakt, iż w pewnych przypadkach użycie go ułatwia implementację, a co za tym idzie ta jest prostsza w odbiorze. Generalnie, poza jakimiś szczególnymi przypadkami, to ten drugi argument powinien przeważać nad tym pierwszym. W tym miejscu należy jednak uważać gdyż nadużycie *volatile* może się w tym kontekście okazać bronią obosieczną.

### Podsumowanie

W tym krótkim artykule został przedstawiony uproszczony i intuicyjny obraz zmiennych *volatile*. o ile tematyka Cię zainteresowała, bardziej formalny opis można znaleźć w specyfikacji języka, którą możesz odnaleźć np. pod tym [linkiem](https://docs.oracle.com/javase/specs/index.html).
W przypadku chęci skorzystania ze zmiennej *volatile* należy zawsze zastanowić się dlaczego chcę to zrobić i czy rzeczywiście gwarancje dostarczane przez Javę dla tych zmiennych są w tym przypadku wystarczające. Zagadnienie *volatile* jest szczegółowo omawiane podczas szkolenia [Wielowątkowość w języku Java](https://www.sages.pl/szkolenia/wielowatkowosc-w-jezyku-java).
Idź do oryginalnego materiału