Współbieżność w Javie: Zaawansowane funkcje
Wprowadzenie
W poprzednim artykule przedstawiliśmy podstawy współbieżności i wielowątkowości w języku Java. Jednak w dynamicznym świecie rozwoju oprogramowania, gdzie responsywność i wydajność są priorytetem, wykorzystanie potęgi wielowątkowości staje się niezbędne. Artykuł „Współbieżność w Javie: Zaawansowane funkcje” zagłębia się w zawiłości programowania równoczesnego, rzucając światło na zaawansowane możliwości oferowane przez pakiet java.util.concurrent.
Podczas gdy podstawy wielowątkowości stanowią fundament, pakiet java.util.concurrent podnosi poziom, dostarczając kompleksowego zestawu narzędzi do radzenia sobie z trudnymi wyzwaniami współbieżności. Ten pakiet pełni rolę skarbnicy specjalistycznych klas i narzędzi, starannie zaprojektowanych w celu usprawnienia operacji równoczesnych, poprawy skalowalności oraz eliminacji potencjalnych pułapek.
Artykuł rozpoczyna podróż mającą na celu odkrycie perełek pakietu java.util.concurrent, oferując wgląd w jego podstawowe klasy i mechanizmy. Od kolekcji bezpiecznych dla wątków po pulę wątków, od prymitywów synchronizacji po abstrakcje na wysokim poziomie – ten pakiet umożliwia programistom korzystanie z potęgi wielowątkowości z precyzją i finezją.
Pakiet java.util.concurrent
Pakiet java.util.concurrent w języku Java to kompleksowy zbiór klas i interfejsów, dostarczający zaawansowanych narzędzi do obsługi programowania wielowątkowego. Pakiet ten został wprowadzony w wersji Java 5, aby radzić sobie ze złożonościami programowania równoczesnego i dostarczyć programistom narzędzi do tworzenia wydajnych, skalowalnych i bezpiecznych wielowątkowych aplikacji. Oferuje on wyższy poziom abstrakcji w porównaniu do tradycyjnych mechanizmów synchronizacji na niskim poziomie.
Pakiet java.util.concurrent obejmuje szeroką gamę klas i interfejsów, z których każdy został zaprojektowany w celu sprostania konkretnym wyzwaniom związanym ze współbieżnością.
Pule wątków
Pakiet ten oferuje klasy, takie jak ExecutorService i ThreadPoolExecutor, które upraszczają zarządzanie pulą wątków roboczych.
Pula wątków w języku Java to zarządzana kolekcją wstępnie zainicjowanych wątków roboczych gotowych do wykonywania zadań równocześnie. Pule wątków służą do zarządzania wykonywaniem wielu zadań w bardziej efektywny sposób niż tworzenie nowego wątku dla każdego zadania. Pomagają one zmniejszyć nakład związany z tworzeniem wątku, kontrolować zużycie zasobów i poprawiać ogólną wydajność aplikacji wielowątkowych.
Java dostarcza framework Executor do pracy z pulami wątków. Najczęściej używaną implementacją Executora jest ExecutorService. Ten framework zarządza pulowaniem wątków, przekazywaniem zadań i wykonywaniem zadań na dostępnych wątkach.
Oto przykład użycia puli wątków z usługą ExecutorService w Javie:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// Create a thread pool with a fixed number of threads
ExecutorService executor = Executors.newFixedThreadPool(5);
// Submit tasks to the thread pool
for (int i = 0; i < 10; i++) {
executor.execute(new Task(i));
}
// Shutdown the thread pool when done
executor.shutdown();
}
}
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is being executed by thread: " + Thread.currentThread().getName());
}
}
wyjście:
Task 1 is being executed by thread: pool-1-thread-2
Task 2 is being executed by thread: pool-1-thread-3
Task 5 is being executed by thread: pool-1-thread-2
Task 6 is being executed by thread: pool-1-thread-3
Task 4 is being executed by thread: pool-1-thread-5
Task 3 is being executed by thread: pool-1-thread-4
Task 0 is being executed by thread: pool-1-thread-1
Task 9 is being executed by thread: pool-1-thread-5
Task 8 is being executed by thread: pool-1-thread-3
Task 7 is being executed by thread: pool-1-thread-2
W tym przykładzie tworzymy pulę wątków przy użyciu Executors.newFixedThreadPool(5), która tworzy pulę złożoną z 5 wątków. Następnie przekazujemy 10 zadań do puli wątków przy użyciu metody execute. Każde zadanie to instancja klasy Task, implementującej interfejs Runnable. Metoda run klasy Task zawiera kod do wykonania przez każdy wątek.
Po przesłaniu wszystkich zadań wywołujemy metodę executor.shutdown(), aby bezpiecznie zamknąć pulę wątków po zakończeniu wszystkich zadań.
Pule wątków są szczególnie przydatne w scenariuszach, gdzie zadania są krótkotrwałe i mogą być wykonane równocześnie. Pomagają one w zarządzaniu liczbą wątków, unikaniu nadmiernego nakładu związanego z tworzeniem i usuwaniem wątków oraz efektywnym wykorzystywaniu zasobów systemowych.
Kolekcje bezpieczne wątkowo
Kolekcje bezpieczne wątkowo w pakiecie java.util.concurrent to struktury danych zaprojektowane do bezpiecznego używania w środowisku wielowątkowym bez konieczności zewnętrznej synchronizacji. Zapewniają, że równoczesny dostęp do kolekcji przez wiele wątków nie prowadzi do niezgodności danych, warunków wyścigowych ani innych problemów związanych z wielowątkowością.
Oto kilka przykładów:
1. ConcurrentHashMap
To wersja bezpieczna wątkowo klasy HashMap. Pozwala wielu wątkom równocześnie uzyskiwać dostęp do mapy bez konieczności jawnej synchronizacji.
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("One", 1);
map.put("Two", 2);
map.put("Three", 3);
int value = map.get("Two");
System.out.println("Value for key 'Two': " + value);
}
}
2. CopyOnWriteArrayList
To bezpieczna wątkowo wersja klasy ArrayList. Pozwala na równoczesne operacje odczytu bez synchronizacji, ale operacje zapisu tworzą nową kopię zasadniczej tablicy.
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
list.add("One");
list.add("Two");
list.add("Three");
for (String item : list) {
System.out.println(item);
}
}
}
3. BlockingQueue
Interfejs BlockingQueue dostarcza bezpieczne wątkowo kolejkowanie, obsługujące blokujące operacje dodawania i usuwania elementów. Popularne implementacje obejmują LinkedBlockingQueue i ArrayBlockingQueue.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
try {
queue.put(1);
queue.put(2);
queue.put(3);
int item = queue.take();
System.out.println("Removed: " + item);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
To jest tylko kilka przykładów bezpiecznych wątkowo kolekcji w Javie. Korzystanie z kolekcji bezpiecznych dla wątków pomaga programistom uniknąć złożoności ręcznej synchronizacji i zapewnia bezpieczny, współbieżny dostęp do struktur danych, dzięki czemu programowanie wielowątkowe jest łatwiejsze w zarządzaniu i mniej podatne na błędy.
Elementy podstawowe synchronizacji
Elementy podstawowe synchronizacji w pakiecie java.util.concurrent to klasy lub mechanizmy, które ułatwiają koordynację i synchronizację między wieloma wątkami, aby zapewnić odpowiednią kolejność wykonania, zapobiec warunkom wyścigowym i zarządzać równoczesnym dostępem do współdzielonych zasobów. Te prymitywy dostarczają strukturyzowany sposób komunikacji między wątkami i synchronizacji ich działań.
Oto przykłady niektórych powszechnie używanych operacji podstawowych synchronizacji w Javie:
1. Semafory
Semafor to element podstawowy synchronizacji, który kontroluje dostęp do współdzielonego zasobu za pomocą zestawu pozwoleń. Wątki mogą pozyskiwać i zwalniać pozwolenia, aby kontrolować swój dostęp do zasobu.
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2); // Initialize with 2 permits
Runnable task = () -> {
try {
semaphore.acquire(); // Acquire a permit
System.out.println(Thread.currentThread().getName() + " is accessing the resource");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " released the resource");
semaphore.release(); // Release the permit
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
2. CountDownLatch
CountDownLatch to element podstawowy synchronizacji, który umożliwia jednemu lub wielu wątkom oczekiwanie, aż określona liczba operacji (liczba) zostanie zakończona przed kontynuacją.
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3); // Initialize with 3 counts
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " is performing a task");
latch.countDown(); // Decrease the count
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
Thread thread3 = new Thread(task);
thread1.start();
thread2.start();
thread3.start();
latch.await(); // Wait until count becomes 0
System.out.println("All tasks are completed");
}
}
3. CyclicBarrier
CyclicBarrier to element podstawowy synchronizacji, który umożliwia zestawowi wątków oczekiwanie na siebie nawzajem przy wspólnym punkcie bariery przed kontynuacją.
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3); // Initialize with 3 parties
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " is waiting at the barrier");
try {
barrier.await(); // Wait at the barrier
System.out.println(Thread.currentThread().getName() + " has passed the barrier");
} catch (Exception e) {
e.printStackTrace();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
Thread thread3 = new Thread(task);
thread1.start();
thread2.start();
thread3.start();
}
}
4. Exchanger
Exchanger to element podstawowy synchronizacji, który dostarcza punkt wymiany danych między dwoma wątkami. Pozwala wątkom na wymianę obiektów, gdy oba wątki osiągną punkt wymiany.
import java.util.concurrent.Exchanger;
public class ExchangerExample {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
Runnable task = () -> {
try {
System.out.println(Thread.currentThread().getName() + " is exchanging data");
String data = exchanger.exchange("Data from " + Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + " received: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
Te przyrządy synchronizacji dostarczają potężnych narzędzi do zarządzania synchronizacją, koordynacją i komunikacją między wątkami, umożliwiając programistom tworzenie wydajnych i dobrze zorganizowanych aplikacji wielowątkowych.
Operacje atomowe
Operacje atomowe w pakiecie java.util.concurrent to operacje, które są zaprojektowane do wykonania atomowego bez ingerencji innych wątków. Te operacje zapewniają, że wartość zmiennej jest odczytywana i aktualizowana w pojedynczym, niepodzielnym kroku, zapobiegając wystąpieniu sytuacji wyścigu i zapewniając bezpieczeństwo wątkowe. Są one niezbędne do zarządzania współdzielonymi danymi w środowiskach wielowątkowych.
Java dostarcza kilka klas w pakiecie java.util.concurrent.atomic do wykonywania operacji atomowych na typach danych pierwotnych i typach referencyjnych. Te klasy korzystają z niskopoziomowego wsparcia sprzętowego, aby zapewnić atomowość bez konieczności stosowania jawnej synchronizacji.
Oto kilka powszechnie używanych klas atomowych i ich operacji:
AtomicInteger
– umożliwia operacje atomowe na wartościach całkowitych.AtomicLong
– umożliwia operacje atomowe na długich wartościach.AtomicReference
– umożliwia operacje atomowe na typach referencyjnych.AtomicBoolean
– umożliwia operacje atomowe na wartościach logicznych.
Operacje atomowe są szczególnie przydatne przy pracy ze współdzielonymi zmiennymi, które muszą być aktualizowane przez wiele wątków jednocześnie. Eliminują one potrzebę jawnej synchronizacji i pomagają w budowaniu wydajnych i bezpiecznych programów wielowątkowych.
Klasy synchronizacji wysokiego poziomu
Klasy synchronizacji wysokiego poziomu w pakiecie java.util.concurrent, takie jak Lock, ReentrantLock i ReadWriteLock, dostarczają bardziej elastyczne i potężne mechanizmy do zarządzania współbieżnym dostępem do współdzielonych zasobów w porównaniu do tradycyjnej synchronizacji za pomocą bloków/metod synchronized. Te klasy oferują większą kontrolę nad blokowaniem, sprawiedliwością i współbieżnością niż wbudowane konstrukcje synchronizacyjne.
Interfejs Lock
Interfejs Lock zapewnia bardziej zaawansowany sposób osiągania synchronizacji niż bloki/metody synchronized. Oferuje metody takie jak lock() i unlock() do jawnej akwizycji i zwalniania blokad.
ReentrantLock
ReentrantLock to implementacja interfejsu Lock, która pozwala wątkowi wielokrotnie blokować i odblokowywać tę samą blokadę, w przeciwieństwie do wbudowanej synchronizacji, która automatycznie ponownie wchodzi w blokadę.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock(); // Acquire the lock
try {
// Critical section
} finally {
lock.unlock(); // Release the lock
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
ReadWriteLock
Interfejs ReadWriteLock dostarcza mechanizmu zarządzania dostępem do współdzielonego zasobu do odczytu i zapisu. Umożliwia wielu wątkom jednoczesny dostęp do odczytu, podczas gdy dostęp do zapisu jest wyłączny.
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
public static void main(String[] args) {
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Runnable readTask = () -> {
rwLock.readLock().lock();
try {
// Read the shared resource
} finally {
rwLock.readLock().unlock();
}
};
Runnable writeTask = () -> {
rwLock.writeLock().lock();
try {
// Modify the shared resource
} finally {
rwLock.writeLock().unlock();
}
};
Thread reader1 = new Thread(readTask);
Thread reader2 = new Thread(readTask);
Thread writer = new Thread(writeTask);
reader1.start();
reader2.start();
writer.start();
}
}
Mechanizmy synchronizacji na wysokim poziomie, takie jak Lock, ReentrantLock i ReadWriteLock, oferują większą kontrolę i elastyczność w zarządzaniu jednoczesnym dostępem do współdzielonych zasobów, ułatwiając tworzenie efektywnych i bezpiecznych wielowątkowych aplikacji.
Podsumowanie
Możliwości współbieżności w języku Java pozwalają programistom tworzyć wydajne aplikacje, wykorzystując potęgę wielowątkowości. Nawigacja w obszarze synchronizacji, zarządzanie współdzielonymi zasobami i orchestracja interakcji między wątkami wymaga głębokiego zrozumienia narzędzi i wyzwań związanych z programowaniem współbieżnym. Poprzez zsynchronizowane metody, zmienne wolatile, blokady i zaawansowane mechanizmy, takie jak pule wątków, Java dostarcza obszerne narzędzia do efektywnego programowania współbieżnego. Zdobywając tę wiedzę, programiści mogą pewnie przyjąć wyzwania świata wielowątkowości, optymalizując aplikacje, aby sprostać wymaganiom nowoczesnego przetwarzania. Dzięki opanowaniu współbieżności w języku Java, będziesz dobrze przygotowany do wykorzystania prawdziwego potencjału równoczesnego wykonania, przekształcając swój kod w efektywne, responsywne i solidne rozwiązania programowe.
Źródła
- https://www.baeldung.com/java-concurrency
- Herbert Schildt, Java: The Complete Reference, Ninth Edition (McGraw-Hill Education Ltd, 2014), chapter 28