Site icon Java blog

Java #59: struktury danych bezpieczne wątkowo

java threadsafe structures

Na koniec cyku wpisów związanych z wielowątkowością, warto przyjrzeć się, jakie struktury danych bezpieczne wątkowo oferuje programistom Java. Okazuje się, że wiele problemów zostało już dawno przez kogoś rozwiązane (co jest dość częste w programowaniu).

Klasy niemodyfikowalne

Pierwszym rodzajem struktur przydatnych do pracy z wątkami są klasy niemodyfikowalne. Są one szczególnie przydatne we wszelkich obiektach immutable*. Główną cechą takich klas, jest fakt, iż po jej utworzeniu instancje nie może się zmieniać. Pozornie taka kolekcja nie ma żadnego sensu. Czasem jednak Twój kod może wymagać, aby utworzyć nowy zbiór z finalnymi wartościami, które nie będzie już mógł nikt przypadkiem aktualizować.

List<String> shoes = new ArrayList<>();  
Collections.addAll(shoes, "Adidas", "Nike", "New balance");  
List<String> finalListOfShoes = Collections.unmodifiableList(shoes);  
System.out.println("Unmodifiable list of shoes: " + finalListOfShoes);  
finalListOfShoes.add("Reebok"); 

Po odpaleniu takiego kodu, wyrzuci on spodziewany wyjątek:

Unmodifiable list of shoes: [Adidas, Nike, New balance]
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.Collections$UnmodifiableCollection.add(Collections.java:1057)
	at advanced.lesson15.ThreadsafeStructure.main(ThreadsafeStructure.java:14)

Tu istnieje jednak ryzyko, bo jeśli wrzucić do kolekcji niemodyfikowalnej inną referencje, to wciąż można modyfikować jej zawartość właśnie przez nią. Spójrz na kolejny przykład:

shoes.add("Reebok");
System.out.println("Modifiable list of shoes: " + finalListOfShoes); 
Modifiable list of shoes: [Adidas, Nike, New balance, Reebok]

Niemodyfikowalna kolekcja uległa modyfikacji. 😉 Wniosek jest bardzo prosty, po „opakowaniu” w kolekcje modyfikowalną wciąż możesz zmieniać poprzednia kolekcję, przez co lista niemodyfikowalna ulegnie też zmianie.

Kolejki blokujące

Z kolejek blokujących niejawnie korzystałem już podczas używania egzekutorów. Interfejs BlockingQueue posiada wiele implementacji, których możesz używać. Na tej lekcji skupię się tylko na jednej z nich, mianowicie na LinkedBlockingQueue. Taka kolejka świetnie rozwiązuje znany w informatyce, producent – konsument**.

Poniżej przedstawiam Ci praktyczne zastosowanie takiej struktury:

public class Producer implements Runnable{
	private BlockingQueue<Integer> queue;
	
	public Producer(BlockingQueue<Integer> queue) {
		this.queue = queue;
	}

	@Override
	public void run() {
		System.out.println("Start producing");
        try {
        	for(int i=0; i<100; i++) {
        	     System.out.println("Produce " + i);
                     queue.put(i);
                     Thread.sleep(100);
        	}
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Producent przyjmuje współdzieloną kolejkę blokującą za pomocą konstruktora. Następnie w pętli for, wypełnia ją kolejnymi wartościami całkowitymi (sleep służy tu tylko, aby zauważyć efekt na konsoli).

public class Consumer implements Runnable{
	private BlockingQueue<Integer> queue;

	public Consumer(BlockingQueue<Integer> queue) {
		this.queue = queue;
	}

	@Override
	public void run() {
		System.out.println("Start consuming");
        try {
        	while(!queue.isEmpty()) {
                System.out.println("Consume " + queue.take());
        	}
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Kolejna klasa, dokonuje pobrania w pętli while wartości z tej samej kolejki, aż do momentu, kiedy wartości będą dostępne***.

public static void main(String[] args) throws InterruptedException {
    BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();

    Producer producer = new Producer(queue);
    Consumer consumer1 = new Consumer(queue);
    Consumer consumer2 = new Consumer(queue);

    new Thread(producer).start();

    Thread.sleep(4000);
    new Thread(consumer1).start();
    new Thread(consumer2).start();
}

W metodzie main startujesz jednego producenta, a po pewnym czasie dołącza do niego dwóch konsumentów. Jeśli spojrzysz na wynik na konsoli, zobaczysz, że oba konsumenci są wstanie pobierać wartości od producenta bez blokowania się wzajemnie. Spróbuj teraz przetestować powyższy kod dla zwykłej kolejki (interfejs Queue). Prawdopodobnie oba konsumenci zablokują się już przy pobieraniu pierwszego elementu.

Klasa CopyOnWriteArrayList

Klasa ta korzysta z techniki, zapewniającej bezpieczeństwo wielowątkowe, poprzez kopiowanie wartości całej zawartości struktury do nowej kopii wewnętrznej. Dzieje się to jednak tylko, gdy wykorzystujesz którąś operację modyfikacji – takie jak add lub remove.

Klasa ConcurrentHashMap

Ostatnia z omawianych kolekcji, przydaje się, gdy potrzebujesz operować na mapach. ConcurrentHashMap zapewnia, że niezależnie ile wątków pracuje na nie, jej struktura nie zostanie uszkodzona. Oczywiście niektóre wątki mogą być czasowo blokowane. Kolekcja ta posiada szereg metod, które działają w sposób atomowy, takich jak: compute, reduce, etc.

Inne struktury danych bezpieczne wątkowo

Przestarzałe kolekcje

Wszystkie poniższe struktury danych bezpieczne wątkowo są zdezaktualizowane i jeśli nie musisz, to postaraj się ich unikać.

*Java zaawansowane #12: obiekty niezmienne (immutable)

**Więcej na temat problemu konsument – producent: https://pl.wikipedia.org/wiki/Problem_producenta_i_konsumenta

***Przeczytaj o problemie zagłodzenia: https://en.wikipedia.org/wiki/Starvation_(computer_science)

Struktury danych bezpieczne wątkowo – link do lekcji: https://github.com/developeronthego/java-advanced/tree/master/src/main/java/advanced/lesson15

Exit mobile version