Programowanie Współbieżne w Języku JAVA

PW 2026L · Kolos: 13.04.2026

Zadania z poprzednich kolosów

Format: 2 zadania kodowania, ok. 20–25 pkt każde. Możesz używać komputera.

Kolos 2025 — Zad. 1 25 pkt
Napisać program wyświetlający dziesięciokrotnie napis „Bonjour" w odstępach jednosekundowych z zachowaniem wysokiej precyzji czasowej.

Rozwiązanie — ScheduledExecutorService

import java.util.concurrent.*;

public class BonjourTask {
    public static void main(String[] args) throws Exception {
        ScheduledExecutorService executor =
            Executors.newScheduledThreadPool(1);
        final int[] count = {0};

        executor.scheduleAtFixedRate(() -> {
            System.out.println("Bonjour");
            count[0]++;
            if (count[0] >= 10) executor.shutdown();
        }, 0, 1, TimeUnit.SECONDS);
    }
}
Dlaczego scheduleAtFixedRate, a nie sleep?

sleep() traci precyzję bo nie uwzględnia czasu wykonania ciała pętli. scheduleAtFixedRate() uruchamia od momentu bazowego co dokładnie 1s.

Kolos 2025 — Zad. 2 25 pkt
Napisać kod klasy MailBox: adresat oczekuje na list. Listonosz czeka na potwierdzenie odbioru przez adresata.

Rozwiązanie — blok strzeżony (wait/notify)

public class MailBox {
    private boolean letterDelivered = false;
    private boolean letterReceived  = false;

    // Listonosz: wkłada list, czeka na potwierdzenie
    public synchronized void deliver() throws InterruptedException {
        letterDelivered = true;
        notifyAll();                      // budzi adresata
        while (!letterReceived) wait(); // czeka na potwierdzenie
        System.out.println("Listonosz: potwierdzono odbiór");
    }

    // Adresat: czeka na list, potwierdza odbiór
    public synchronized void receive() throws InterruptedException {
        while (!letterDelivered) wait(); // czeka na list
        letterReceived = true;
        notifyAll();                      // budzi listonosza
        System.out.println("Adresat: odebrałem list");
    }
}
Poprawa 2025 — Zad. 1 20 pkt
Napisać kod równoważny klasycznej synchronizacji, ale z użyciem zamków zewnętrznych (ReentrantLock).

Oryginalny kod (synchronized) → ReentrantLock

import java.util.concurrent.locks.*;

public class AClass {
    private final Lock lock = new ReentrantLock();

    public void method1() {
        lock.lock();
        try {
            System.out.println("instrukcja1");
        } finally {
            lock.unlock(); // zawsze w finally!
        }
    }

    public void method2() {
        lock.lock();
        try {
            System.out.println("instrukcja2");
        } finally {
            lock.unlock();
        }
    }
}

Jeden zamek dla obu metod — tak jak synchronized na this, method1 i method2 wzajemnie się blokują. Dwa osobne zamki byłyby BŁĘDEM — metody mogłyby działać równolegle, co nie jest równoważne oryginałowi.

Poprawa 2025 — Zad. 2 20 pkt
Dwa wątki: thread1 czeka 1s → przerywa thread2. Thread2 czeka 2s → jeśli przerwany przed końcem: przerywa thread1.
public class Main {
    public static void main(String[] args) {
        Thread[] ref = new Thread[2];

        ref[0] = new Thread(() -> {
            try {
                Thread.sleep(1000);
                ref[1].interrupt();  // przerywa thread2
                Thread.sleep(5000);  // czeka — musi żyć żeby thread2 mógł go przerwać!
            } catch (InterruptedException e) {
                System.out.println("thread1 przerwany przez thread2");
            }
        });

        ref[1] = new Thread(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("thread2: 2s upłynęły");
            } catch (InterruptedException e) {
                System.out.println("thread2 przerwany przed 2s");
                ref[0].interrupt();  // przerywa thread1
            }
        });

        ref[0].start();
        ref[1].start();
    }
}

Kluczowe: thread1 po przerwaniu thread2 musi dalej czekać (sleep), żeby był jeszcze żywy kiedy thread2 spróbuje go przerwać. Bez drugiego sleep → thread1 jest TERMINATED i interrupt() nic nie robi.

Tworzenie i cykl życia wątków

Cykl życia wątku

NEW
→ start()
RUNNABLE
↔ OS
RUNNING
RUNNING
→ wait()/join()/sleep()
WAITING / TIMED_WAITING
RUNNING
→ brak zamka
BLOCKED
→ zamek wolny
RUNNABLE
RUNNING
→ koniec run()
TERMINATED

1. implements Runnable

public class MojWatek implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable");
    }
    public static void main(String[] args) {
        new Thread(new MojWatek()).start();
    }
}

✅ Preferowane — pozwala dziedziczyć po innej klasie

2. extends Thread

public class MojWatek extends Thread {
    @Override
    public void run() {
        System.out.println("Thread");
    }
    public static void main(String[] args) {
        new MojWatek().start();
    }
}

⚠️ Brak wielodziedziczenia w Javie!

3. Klasa anonimowa Runnable

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("anon");
    }
}).start();

4. Lambda

new Thread(
  () -> System.out.println("lambda")
).start();

Najkrótsza forma!

5. Referencja do metody

new Thread(MyClass::myMethod).start();

Gdy metoda już istnieje


Metody klasy Thread

MetodaOpisUwaga
start()Uruchamia wątek (wywołuje run() w nowym wątku)OK
join()Czeka na zakończenie wątkuOK
sleep(ms)Zawiesza bieżący wątekOK
isAlive()Czy wątek jeszcze działa?OK
interrupt()Wysyła sygnał przerwaniaOK
stop()Zatrzymuje wątekDEPRECATED
suspend()Zawiesza wątekDEPRECATED
resume()Wznawia wątekDEPRECATED

Synchronizacja dostępu do zasobów

Problem: interferencja wątków

Operacja a++ NIE jest atomowa — składa się z 3 kroków: odczyt → dodaj → zapis. Przy 2 wątkach może nastąpić wyścig i utrata danych.

A: pobierz a=0
B: pobierz a=0
A: dodaj 1 → 1
B: odejmij 1 → -1
A: zapisz a=1
B: zapisz a=-1 ← zły wynik!

Operacje atomiczne (z definicji)

  • Odczyt/zapis zmiennych referencyjnych
  • Odczyt/zapis typów prymitywnych (poza long i double)
  • Odczyt/zapis zmiennych volatile (w tym long/double)

a++ NIE jest atomowa! Używaj AtomicInteger.

1. synchronized method

public synchronized void zwieksz() {
    wartosc++;
}

Zamek na this. Tylko 1 wątek naraz w metodzie.

2. synchronized block

public void zwieksz() {
  synchronized(this) {
    wartosc++;
  }
  // reszta niesync
}

Precyzyjniejsza kontrola. Lepszy performance.

3. ReentrantLock

Lock lock = new ReentrantLock();
lock.lock();
try {
    wartosc++;
} finally {
    lock.unlock(); // !
}

ReentrantLock — metody

MetodaOpis
lock()Blokuje (czeka jeśli zajęty)
unlock()Zwalnia zamek — ZAWSZE w finally!
tryLock()Próbuje zamek, zwraca boolean (nie blokuje)
tryLock(t, unit)Próbuje przez czas t
lockInterruptibly()Jak lock(), ale można przerwać sygnałem

Producent – Konsument

public synchronized void dodaj(int v)
    throws InterruptedException {
  while (queue.size() == MAX) wait();
  queue.add(v);
  notifyAll();
}
public synchronized int pobierz()
    throws InterruptedException {
  while (queue.isEmpty()) wait();
  int v = queue.poll();
  notifyAll();
  return v;
}

Zmienne atomiczne — java.util.concurrent.atomic

AtomicInteger — metody
get() set(v) addAndGet(delta) incrementAndGet() decrementAndGet()
Współbieżne kolekcje
ConcurrentHashMap CopyOnWriteArrayList Semaphore CountDownLatch CyclicBarrier

Oddziaływania między wątkami

Przerwania — interrupt()

  • interrupt() — wysyła sygnał przerwania wątkowi
  • isInterrupted() — sprawdza flagę (nie czyści)
  • interrupted() — sprawdza i czyści flagę (statyczna)

Metody blokujące (sleep, wait, join) rzucają InterruptedException i czyszczą flagę na false.

public void run() {
  while (true) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException ie) {
      // obsłuż przerwanie
      return;
    }
  }
}

Blok strzeżony — wait/notify

  • wait() — zwalnia zamek i czeka
  • notify() — budzi jeden czekający wątek
  • notifyAll() — budzi wszystkie czekające wątki

⚠️ Można wywoływać TYLKO wewnątrz synchronized!

public synchronized void guardedCondition()
    throws InterruptedException {
  while (!warunek) {
    wait(); // zawsze w pętli while!
  }
  // warunek spełniony
}
public synchronized void notifyCondition() {
  warunek = true;
  notifyAll();
}

Metoda join()

Zawiesza bieżący wątek do momentu zakończenia innego wątku. Wewnętrznie używa bloku strzeżonego z wait().

Wersje
join() join(long timeout) join(long timeout, int nanos)
// Thread t = ...;
t.start();
t.join(); // czeka aż t się skończy
System.out.println("t zakończony");

sleep() vs wait() — różnice

sleep()wait()
KlasaThread (statyczna)Object (instancja)
ZamekNIE zwalnia zamkaZwalnia zamek
BudziPo upływie czasunotify() / notifyAll()
KontekstWszędzieTylko w synchronized

Wielowątkowość wysokopoziomowa

ExecutorService

// Pula 4 wątków
ExecutorService exec =
  Executors.newFixedThreadPool(4);

// Callable zwraca wynik
Callable<String> zadanie = () -> "wynik";

Future<String> future = exec.submit(zadanie);
String wynik = future.get(); // blokuje

exec.shutdown(); // zawsze!
Typy pul
newFixedThreadPool(n) newSingleThreadExecutor() newCachedThreadPool()

ScheduledExecutorService

ScheduledExecutorService sched =
  Executors.newScheduledThreadPool(1);

// Jednorazowo po 5s
sched.schedule(task, 5, TimeUnit.SECONDS);

// Co 1s, pierwsze od razu
sched.scheduleAtFixedRate(
    task, 0, 1, TimeUnit.SECONDS);

// Co 1s od końca poprzedniego
sched.scheduleWithFixedDelay(
    task, 0, 1, TimeUnit.SECONDS);

Fork-Join Framework

class SumTask extends RecursiveTask<Integer> {
  @Override
  protected Integer compute() {
    if (koniec - start < 10) {
      return /* oblicz sekwencyjnie */;
    }
    int mid = (start + koniec) / 2;
    SumTask t1 = new SumTask(tab, start, mid);
    SumTask t2 = new SumTask(tab, mid, koniec);
    invokeAll(t1, t2);
    return t1.join() + t2.join();
  }
}
ForkJoinPool pool = new ForkJoinPool();
int wynik = pool.invoke(new SumTask(...));

RecursiveTask → zwraca wynik; RecursiveAction → bez wyniku

Strumienie — stream() / parallelStream()

List<Integer> liczby = Arrays.asList(1,2,3,4,5);

// Sekwencyjny
int suma = liczby.stream()
    .mapToInt(Integer::intValue)
    .sum();

// Równoległy — wiele wątków
int sumaR = liczby.parallelStream()
    .mapToInt(Integer::intValue)
    .sum();
Interfejs/KlasaMetoda kluczowaZwracaOpis
Runnablerun()voidProsta operacja, brak wyniku
Callable<V>call()VOperacja zwracająca wynik
Future<V>get()VPobiera wynik (blokuje)
RecursiveTask<V>compute()VFork-Join z wynikiem
RecursiveActioncompute()voidFork-Join bez wyniku

Problemy synchronizacji

💀 Deadlock

Dwa wątki wzajemnie blokują się w nieskończoność — każdy czeka na zamek trzymany przez drugiego.

// A trzyma lock1, czeka na lock2
// B trzyma lock2, czeka na lock1
// → wieczna blokada
Zapobieganie
  • Zawsze blokuj zamki w tej samej kolejności
  • Używaj tryLock() z timeoutem
  • Jeden globalny zamek

🍽️ Starvation

Wątek nie dostaje dostępu do zasobu, bo inne wątki są „zachłanne" i zajmują go nieustannie.

Np. metoda synchronized wykonuje się długo i jest często wywoływana przez inny wątek — reszta ciągle zablokowana (BLOCKED).

Rzadki scenariusz

Używaj fair lock: new ReentrantLock(true)

🔄 Livelock

Wątki reagują na siebie nawzajem i uniemożliwiają postęp — choć nie są zablokowane (aktywnie „działają").

Analogia: dwie osoby w wąskim korytarzu ustępują sobie w nieskończoność, żadna nie przejdzie.

Też rzadki

Wprowadź losowe opóźnienie lub priorytety.


Przykład Deadlock — klasyczny (Oracle)

class Friend {
    public synchronized void bow(Friend bower) {
        System.out.println(name + ": " + bower.name + " ukłonił się");
        bower.bowBack(this); // ← próbuje wejść w synchronized bowera!
    }
    public synchronized void bowBack(Friend bower) { ... }
}
// Thread1: alphonse.bow(gaston) → trzyma zamek alphonse, czeka na zamek gaston
// Thread2: gaston.bow(alphonse) → trzyma zamek gaston, czeka na zamek alphonse
// DEADLOCK

Relacja happens-before

Fiszki — kliknij żeby odkryć

0 / 16 kart odkrytych
Dwie metody tworzenia wątków w Javie?
kliknij
1. implements Runnable + new Thread(r).start()
2. extends Thread + .start()
Co robi Thread.start() vs Thread.run()?
start() → tworzy nowy wątek i wywołuje run() w nim
run() → wywołuje metodę w bieżącym wątku
6 stanów cyklu życia wątku?
NEW → RUNNABLE → (RUNNING)
→ WAITING / TIMED_WAITING / BLOCKED
→ TERMINATED
Co to interferencja wątków?
Gdy operacje z różnych wątków (złożone z wielu kroków) nakładają się i powodują błędne wyniki — race condition.
Różnica synchronized method vs synchronized block?
Method: zamek na cały obiekt (this), całe ciało
Block: wskazujesz obiekt, synchronizujesz fragment — lepsza wydajność
Dlaczego unlock() musi być w finally?
Aby zamek zawsze był zwolniony — nawet jeśli try rzuci wyjątek. Bez tego → deadlock.
Co robi wait() i gdzie można jej użyć?
Zwalnia zamek i zawiesza wątek. Można TYLKO wewnątrz synchronized. Budzi ją notify() / notifyAll().
Dlaczego wait() zawsze w pętli while, nie if?
Spurious wakeup — wątek może się obudzić bez notify(). Pętla sprawdza warunek ponownie.
Różnica sleep() vs wait()?
sleep(): Thread, nie zwalnia zamka, czas
wait(): Object, ZWALNIA zamek, czeka na notify()
Co robi interrupt() i co się dzieje ze sleep()?
Ustawia flagę przerwania. Jeśli wątek śpi → rzuca InterruptedException i czyści flagę.
Callable vs Runnable?
Runnable: run() → void, brak wyjątku
Callable<V>: call() → V, może rzucić Exception
scheduleAtFixedRate vs scheduleWithFixedDelay?
AtFixedRate: kolejne od momentu startu (stały rytm)
WithFixedDelay: kolejne od końca poprzedniego
RecursiveTask vs RecursiveAction?
RecursiveTask<V>: compute() zwraca wynik V
RecursiveAction: compute() → void
Czym jest Deadlock?
Dwa+ wątki wzajemnie blokują się — każdy czeka na zamek trzymany przez drugi. Trwa w nieskończoność.
Różnica: Deadlock / Starvation / Livelock?
Deadlock: zablokowane, czekają wzajemnie
Starvation: jeden głoduje (inni zachłanni)
Livelock: działają ale nie postępują
Co to operacja atomowa? Czy a++ jest atomowa?
Niepodzielna — wykonuje się cała lub wcale. a++ NIE jest atomowa (3 kroki: odczyt, +1, zapis).