Praca w wielowątkowym środowisku stwarza wiele możliwości, ale też problemów. Jednym z nim jest synchronizacja wątków pracujących na tych samych zasobach. Częstym problemem, pojawiającym się w programowaniu współbieżnym, jest tzw. wyścig (ang. race condition). Pojawia się on wtedy, gdy wiele wątków wykonuje swoją pracę na tych samej sekcji krytycznej bez synchronizacji. Świetnym przykładem takiego zachowania jest stosowanie licznika. Załóżmy, że dwa wątki pracują na tym samej zmiennej. Aby dokonać inkrementacji, każdy z nich musi odczytać aktualną wartość, a następnie ją zwiększyć. Problem pojawia się, gdy oba wątki dokonają odczytu w tym samym momencie, zamiast pozwolić tylko jednemu z nich na wykonanie inkrementacji w tym samym czasie. Spójrz na poniższy przykład, aby zrozumieć, jak synchronizacja wątków jest ważna.
Implementacja współdzielonego licznika
public class Counter { private static int counter = 0; public void incrementCounter() { counter++; System.out.println(Thread.currentThread().getName() + " : " + counter); } public static int getCounter() { return counter; } }
Zaimplementowałem tu zwykła klasę, przechowującą liczbę całkowitą jako zmienną statyczną. Dodatkowo klasa zawiera getter oraz metodę, która dokonuje inkrementacji i wyświetla jej wartość. Mój licznik musi być polem statycznym, ponieważ inaczej nie będzie widoczny dla innych wątków.
Teraz przetestuję jak mój licznik sprawdzi się w środowisku wielowątkowym.
public class CounterThread extends Thread { private static final int LIMIT = 20; @Override public void run() { Counter counter = new Counter(); while (Counter.getCounter() < LIMIT) { counter.incrementCounter(); } } }
Powyższy wątek będzie inkrementował licznik aż do osiągnięcie limitu w wysokości liczby 20. Poniżej metoda main, zawierająca trzy instancje tego samego wątku.
public class ThreadStarter { public static void main(String[] args) { CounterThread firstCounter = new CounterThread(); firstCounter.setName("number one"); firstCounter.start(); CounterThread secondCounter = new CounterThread(); secondCounter.setName("number two"); secondCounter.start(); CounterThread thirdCounter = new CounterThread(); thirdCounter.setName("number three"); thirdCounter.start(); } }
Wynik na konsoli:
number three : 3 number three : 4 number three : 5 number three : 6 number three : 7 number three : 8 number three : 9 number three : 10 number three : 11 number three : 12 number three : 13 number one : 3 number one : 15 number one : 16 number one : 17 number one : 18 number one : 19 number one : 20 number two : 3 number three : 14
Jak widzisz, efektem jest kompletny chaos. Każdy wątek pracuje „jak chce”. Wątek numer dwa praktycznie został wyłączony z pracy przez pozostałe.
Synchronizacja wątków poprzez synchronized
Jak uchronić się przed taką sytuacją? Najprostszą możliwością jest właśnie synchronizacja wątków. Służy do tego słowo synchronized, które można wykorzystać na kilka sposobów.
- Wykorzystanie synchronized na obiekcie bieżącym (np. synchronized (this)). Blokuje wtedy cały obiekt (ale nie klasę!). Przydatne, gdy synchronizacji ma podlegać każdy z obiektów osobno.
- Użycie synchronized w sygnaturze metody (np. public static synchronized int calculate()). Ma to sens, gdy wiele wątków wywołuje tą samą metodę. Należy pamiętać, że zachowanie będzie różne w zależności, czy jest to metoda statyczna czy nie. Synchronizacji będzie tu podlegać jedynie ciało metody i nic więcej.
- Ostatnią opcją, jest synchronizowanie bloku poprzez skorzystanie z jakiegoś pola danej klasy.
Synchronizacja konkretnego bloku
W tym przykładzie, wykorzystam synchronizacje na polu statycznym. Powodem tego jest to, że mój licznik jest statyczny, także synchronizacja na obiekcie lub metodzie, nie ma tu sensu. Teoretycznie mógłbym zablokować sam licznik, jednak po pierwsze jest to typ prosty a nie obiektowy, a po drugie nie korzystam tu ze zmiennej typu atomowego (ale o tym więcej w następnej lekcji).
public class SynchronizedCounter { private static int counter = 0; private static final Object lock = new Object(); public void incrementCounter() { synchronized (lock) { counter++; System.out.println(Thread.currentThread().getName() + " : " + counter); } } public static int getCounter() { return counter; } }
Warto tutaj zwiększyć licznik do 1000, żeby zauważyć że wszystkie wątku faktyczne pracują.
Fragment mojej konsoli:
number one : 1 number one : 2 number one : 3 number one : 4 number one : 5 number one : 6 number one : 7 number three : 8 number three : 9 number three : 10 number three : 11 number three : 12 number three : 13 number three : 14 number three : 15 number three : 16 number three : 17 number three : 18
Faktycznie, tym razem licznik jest synchronizowany. Użycie słowa synchronized, jest najprostszym, zagwarantowanym przez kompilator, sposobem na synchronizację wątków. Jednak niejedynym. W kolejnych lekcjach pokażę Ci specjalne, dedykowane już do pracy na wielu wątkach, klasy.
Lekcja na githubie: https://github.com/developeronthego/java-advanced/tree/master/src/main/java/advanced/lesson10
Jeśli nie rozumiesz pojęcia wątku, to wróć do przedniej lekcji: Java #53: wątki – przetwarzanie wielowątkowe