Java zaawansowane #11: zmienne atomowe i zamki

Poza możliwością synchronizacji wątków, używając słowa sychronized, Java udostępnia wiele dedykowanych rozwiązań związanych z pracą w środowisku wielowątkowym. Większość z nich znajdziesz w pakiecie java.util.concurrent. W tym wpisie omówię dwa z nich: zmienne atomowe (ang. atomic variable) oraz zamki (ang. locks).

Zmienne atomowe

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;
	}
}

Postaram się zoptymalizować powyższy kod. Jak wiesz, synchronized zadziała jedynie na obiektach, w takim razie musimy zastąpić jakoś int jako typ licznika. Najprostszą opcją byłoby zastosowanie zwykłego Integer, tak jak poniżej:

public class AtomicCounter {
    private static Integer counter = new Integer(0);

    public void incrementCounter() {
        synchronized (counter) {
            System.out.println(Thread.currentThread().getName() + " : " + counter);
            counter++;
        }
    }
    
    public static int getCounter() {
		return counter;
	}
}

Inkrementacja a wielowątkowość

Teraz kod wygląda już lepiej, z racji, że nie występuje w nim zmienna pomocnicza lock. Można przeprowadzić synchronizację na samym liczniku, jednak zachodzi tu pewien problem. Ponieważ licznik typu całkowitego może wykonać jedną operację atomową w tym samym czasie, należy sobie odpowiedzieć na pytanie, czy inkrementacja jest taką operacją? Otóż to, co dzieje się pod spodem, to odczytanie wartości, zwiększenie jej o jeden, oraz przypisanie nowej wartości do zmiennej. Skoro tak, klasy opakowujące typy proste (takie jak Integer, Double) nie nadają się do pracy na wielu wątkach. Z pomocą za to przychodzą tzw. zmienne atomowe, czyli specjalnie przygotowane wrappery na typy proste, które świetnie sprawdzają się przy pracy na wielu wątkach. Przykładem takiej zmiennej jest np. AtomicInteger.

public class AtomicCounter {
    private static AtomicInteger counter = new AtomicInteger(0);

    public void incrementCounter() {
        synchronized (counter) {
            System.out.println(Thread.currentThread().getName() + " : " + counter);
            counter.incrementAndGet();
        }
    }
    
    public static int getCounter() {
		return counter.get();
	}
}

IncrementAndGet vs getAndIncrement

Teraz kod korzysta nie tylko z synchronizacji, ale także ze zmiennej AtomicInteger, która umożliwia wykonanie inkrementacji w środowisku wielowątkowym. Dostępne są tu dwie metody: incrementAndGet (najpierw zwiększa wartość a później ją przypisuje) oraz getAndIncrement (pozostawia starą wartość w obecnej iteracji i zwiększy ją w następnej). Pamiętaj, że nie są one tożsame i możesz łatwo popełnić tu błąd.

Ostatnim krokiem będzie skorzystanie z napisanych już w poprzednim przykładzie klas ThreadStarter oraz CounterThread i sprawdzenie, czy nowy licznik działa poprawnie. Pamiętaj, że w klasie CounterThread musisz odnosić się do nowej implementacji.

number one : 0
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
number three : 19
number two : 20

Pamiętaj, że wyniki na Twoim komputerze mogą być trochę inne, z racji, że kompilator będzie optymalizował wykorzystanie wątków w zależności od dostępnych zasobów sprzętowych. Najważniejsze w tym przykładzie jest, aby licznik cały czas wzrastał o jeden.

Zamki

Ostatnim sposobem synchronizacji, który Ci pokażę, jest skorzystanie z zamka. W pakiecie concurrent istnieje kilka jego implementacji. Tu skupię się na najpopularniejszej z nich, czyli klasie ReentrantLock.

class CounterLock {
    private static int counter = 0;
    private static final ReentrantLock lock = new ReentrantLock();

    public void incrementCounter() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " : " + counter);
            counter++;
        } finally {
        	lock.unlock();
        }
    }
    
    public static int getCounter() {
		return counter;
	}
}

Ta implementacja znów bardzo przypomina kod z początku lekcji. Podstawową różnicą jest to, że tym razem zamiast korzystania z wartości atomowych lub synchronizowanego pola pomocniczego, używam klasy ReentrantLock. Pozwala mi ona na zabezpieczenie sekcji krytycznej, aż do momentu, kiedy zwolnię zmienną counter. Teoretycznie nie musisz korzystać z bloku try – finally. Jednak taki zapis zabezpieczy Cię przed przypadkiem, kiedy to zamek nigdy nie zostanie otworzony dla innych wątków.

Inne synchronizatory

Poza zamkami i zmiennymi atomowymi, Java posiada jeszcze wiele innych sposób synchronizacji wątków. Nie będę jednak ich opisywał w tym kursie. Inne możliwości to np. semafory (ang. semaphores) i bariery (ang. barriers). Zachęcam Cię do poznania tych struktur już we własnym zakresie.

Link do kodu: https://github.com/developeronthego/java-advanced/tree/master/src/main/java/advanced/lesson11

Semafory: https://mkyong.com/java/java-thread-mutex-and-semaphore-example/

Bariery: https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/CyclicBarrier.html

Może Ci się również spodoba

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *