Współbieżność w Javie – synchronizacja i wielowątkowość

Tomasz Niegowski

Java jest potężnym narzędziem umożliwiającym programistom tworzenie solidnych i wydajnych aplikacji. Istotnym aspektem jej potęgi jest zdolność do obsługi równoczesnego wykonania poprzez wielowątkowość. Czytając ten artykuł, przenikniesz głęboko w świat współbieżności w Javie, a także poznasz niuanse synchronizacji, wielowątkowości i sztuki harmonizacji równoległego wykonania. Odkryjesz również wyzwania stawiane przez programowanie współbieżne, mechanizmy radzenia sobie z nimi oraz praktyczne przykłady kodu, aby zilustrować te koncepcje.

Zrozumienie współbieżności i wielowątkowości

Współbieżność to zdolność systemu do wykonywania wielu zadań jednocześnie. Wielowątkowość jest natomiast techniką, która umożliwia jednemu procesowi posiadanie wielu wątków – każdy z nich działa niezależnie, jednocześnie dzieląc te same zasoby. Choć to równoczesne wykonanie przynosi efektywność, wprowadza złożoności z powodu potencjalnych konfliktów i wyścigów danych.

Potrzeba synchronizacji

Kiedy kilka wątków jednocześnie korzysta ze współdzielonych zasobów, pojawiają się problemy, takie jak warunki wyścigowe i niezgodności danych. Java dostarcza mechanizmów synchronizacji do organizowania interakcji między wątkami, zapewniając właściwą koordynację i integralność danych.

Podstawowe wbudowane wsparcie dla programowania wielowątkowego w Javie

W Javie znajdziesz wbudowane wsparcie dla programowania wielowątkowego za pomocą klasy java.lang.Thread i pakietu java.util.concurrent. Pozwala to na tworzenie i zarządzanie wieloma wątkami wykonania w ramach pojedynczej aplikacji Javy. Dzięki temu możliwe jest równoczesne wykonywanie zadań i poprawa wydajności aplikacji poprzez wykorzystanie dostępnych rdzeni procesora. Poniżej prezentuję kilka podstawowych funkcji i klas związanych z programowaniem wielowątkowym w Javie.

java.lang.Thread

Klasa Thread stanowi rdzeń wsparcia wielowątkowości w Javie. Reprezentuje pojedynczy wątek wykonania w ramach tego programu. Jak utworzyć nowy wątek? Albo rozszerzając klasę Thread i nadpisując jej metodę run(), albo przekazując obiekt Runnable do konstruktora Thread.

class ThreadExample extends Thread {

  @Override
  public void run() {
    System.out.println("We are in new Thread!");
  }
}

class ThreadTest {
  public static void main(String[] args) {
    Thread thread = new ThreadExample();
    thread.start();
  }
}

Rezultat:

We are in new Thread!
We are in another Thread!

Interfejs Runnable

Runnable reprezentuje zadanie, które może być wykonane równocześnie. Posiada jedną metodę run(), którą musisz zaimplementować, aby zdefiniować zachowanie zadania. Implementując interfejs Runnable, możesz odseparować logikę zadania od logiki zarządzania wątkiem.

class MyRunnable implements Runnable {

  @Override
  public void run() {
    System.out.println("We are in Thread " + Thread.currentThread().getName());
  }
}

class ThreadTest {
  public static void main(String[] args) {
    Runnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    thread.start();

//    starting new Thread on the fly
    new Thread(new MyRunnable()).start();
  }
}

Rezultat:

We are in Thread Thread-0
We are in Thread Thread-1

W obu podejściach metoda run() definiuje kod, który zostanie wykonany przez nowy wątek. Metoda start() służy do rozpoczęcia wykonania wątku. Po wywołaniu metody start() metoda run() nowego wątku jest wykonywana równocześnie z wątkiem głównym lub innymi wątkami, które mogą być uruchomione.

Warto zauważyć, że tworzenie i uruchamianie nowego wątku wiąże się z pewnym nakładem pracy, dlatego zaleca się często korzystanie z puli wątków (poprzez klasy takie jak ExecutorService) w celu efektywnego zarządzania wieloma wątkami.

Stany wątków

W Javie wątki mogą istnieć w różnych stanach podczas swojego cyklu życia. Reprezentują one różne etapy, przez które wątek przechodzi od utworzenia do zakończenia. Zrozumienie stanów wątków jest kluczowe dla efektywnego programowania wielowątkowego. Poniżej prezentuję różne stany wątków w Javie.

  • Stan „nowy”

Wątek znajduje się w stanie „nowy” po utworzeniu, ale przed uruchomieniem. W tym stanie metoda start() wątku nie jest jeszcze wywoływana.

  • Stan „wykonywalny”

Po wywołaniu metody start() wątek przechodzi do stanu „wykonywalnego”. W tym stanie wątek może zostać wykonany przez procesor, ale może nie być aktywnie uruchomiony w każdej chwili ze względu na harmonogram procesora.

  • Stan „uruchomiony”

Kiedy program planujący procesora wybiera wątek z puli „wykonywalnej” do wykonania, wątek przechodzi w stan „uruchomiony”. Aktywnie wykonuje swój kod.

  • Stan „zablokowany” (lub „oczekujący”)

Wątek może przejść do stanu „zablokowanego”, jeśli oczekuje, aż spełni się określony warunek. Może to wystąpić, gdy wątek oczekuje na operacje wejścia/wyjścia, blokady synchronizacyjne lub inne zdarzenia. Po spełnieniu warunku wątek może powrócić do stanu „wykonywalnego”.

  • Stan „oczekiwania z opóźnieniem”

Wątek może wejść w stan „oczekiwania z opóźnieniem”, jeśli oczekuje przez określony czas. Ten stan często występuje podczas korzystania z metod takich jak Thread.sleep() lub oczekiwania na zakończenie wątku za pomocą metod takich jak join().

  • Stan „zakończony” („martwy”)

Wątek przechodzi w stan „zakończony” po zakończeniu jego metody run() lub gdy w wątku wystąpi nieobsługiwany wyjątek. Po zakończeniu wątek nie może być ponownie uruchomiony ani wznowiony.

Co jeszcze warto wiedzieć o stanach wątków?

Warto zauważyć, że wątki mogą przechodzić między tymi stanami na podstawie czynników takich jak planowanie CPU, synchronizacja i operacje wejścia/wyjścia. Aby skutecznie zarządzać tymi przejściami i zapobiec problemom takim jak zakleszczenia czy warunki wyścigowe, należy używać odpowiednich mechanizmów synchronizacji i technik komunikacji międzywątkowej.

Monitorowanie i zrozumienie stanów wątków są kluczowe dla debugowania i optymalizacji aplikacji wielowątkowych. Java dostarcza narzędzi takich jak zrzuty wątków i profile, które pomagają zidentyfikować stany wątków i potencjalne problemy w aplikacji.

Synchronizacja wątków

To fundamentalne pojęcie w programowaniu wielowątkowym, które zapewnia, że wiele wątków może mieć uporządkowany i kontrolowany dostęp do współdzielonych zasobów lub krytycznych sekcji kodu. Zapobiega to warunkom wyścigowym, gdzie zachowanie programu zależy od względnego czasu zdarzeń w wielu wątkach.

W Javie synchronizację osiąga się głównie za pomocą słowa kluczowego „synchronized” wraz z metodami wait(), notify() i notifyAll() dostarczanymi przez klasę Object. Mechanizmy te umożliwiają wątkom koordynowanie swoich działań i unikanie problemów, takich jak uszkodzenie danych i zakleszczenia.

Mechanizmy synchronizacji z przykładami

  1. Słowo kluczowe synchronized

Słowo kluczowe „synchronized” pozwala tworzyć sekcję krytyczną, znanej również jako blok lub metoda synchronizowana. Blok synchronized wykonuje tylko jeden wątek w danym czasie, zapewniając kontrolowany dostęp do współdzielonych zasobów.

Przykład użycia bloku synchronized:

public void synchronizedMethod() {
  // Only one thread can enter this block at a time
  synchronized (this) {
    // Critical section of code
  }
}
  1. Metody wait(), notify() i notifyAll()

Metody te dostarczają sposób komunikacji między wątkami oraz koordynacji ich działań. Należy ich używać w blokach synchronized.

  • wait() – powoduje, że bieżący wątek zwalnia blokadę i czeka, aż inny wątek wywoła notify() lub notifyAll() na tym samym obiekcie.
  • notify() – budzi jeden wstrzymywany wątek (jeśli istnieje taki), który został zablokowany na wywołaniu metody wait() tego samego obiektu.
  • notifyAll() – budzi wszystkie oczekujące wątki, które zostały zablokowane na wywołaniu metody wait() tego samego obiektu.

Przykład użycia wait() i notify():

class SharedResource {
  private boolean flag = false;

  synchronized void produce() {
    while (flag) {
      try {
        wait(); // Releases the lock and waits
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    // Produce data
    flag = true;
    notify(); // Notifies a waiting thread
  }

  synchronized void consume() {
    while (!flag) {
      try {
        wait(); // Releases the lock and waits
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    // Consume data
    flag = false;
    notify(); // Notifies a waiting thread
  }
}

W powyższym przykładzie dwa wątki współdziałają z zasobem wspólnym, korzystając z synchronizacji i komunikacji międzywątkowej. Jeden wątek wykonuje metodę produce(), a drugi metodę consume(). Mechanizmy wait() i notify() zapewniają, że wątki wykonują się na przemian, unikając nadmiernego konsumowania lub produkcji.

O czym pamiętać przy synchronizacji wątków?

Synchronizacja wątków jest istotna dla utrzymania spójności danych, unikania warunków wyścigowych i zapewniania poprawnego wykonania programów wielowątkowych. Jednak niewłaściwe użycie synchronizacji może prowadzić do zakleszczeń lub niewydajnego działania, dlatego ważne jest staranne zaprojektowanie synchronizacji na podstawie wymagań aplikacji.

Podsumowanie

W artykule przedstawiłem podstawowe informacje na temat znaczenia wielowątkowości i kluczowej roli, jaką odgrywa synchronizacja w tworzeniu niezawodnych, responsywnych i wydajnych aplikacji Java. Teraz, gdy masz już niezbędną wiedzę, możesz wyruszyć w podróż mającą na celu opanowanie programowania współbieżnego w środowisku Java.

Referencje

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami