Java Concurrency - Pytania Rekrutacyjne dla Senior Developer [2026]

Sławomir Plamowski 19 min czytania
backend concurrency executors java pytania-rekrutacyjne threads wielowątkowość

Wielowątkowość to jeden z najtrudniejszych obszarów Javy i jednocześnie jeden z najczęściej sprawdzanych na rozmowach na stanowiska senior. Rekruterzy pytają nie tylko o podstawy (Thread, synchronized), ale też o zaawansowane koncepty: memory model, happens-before, lock-free programming, CompletableFuture. Ten przewodnik zawiera 50+ pytań rekrutacyjnych z odpowiedziami, które pomogą Ci przygotować się do rozmowy.

Podstawy wątków

Czym różni się Thread od Runnable?

Odpowiedź w 30 sekund:

  • Thread - klasa, rozszerzasz ją i nadpisujesz run()
  • Runnable - interfejs funkcyjny z jedną metodą run()

Runnable jest preferowany bo pozwala na dziedziczenie z innej klasy.

Odpowiedź w 2 minuty:

W Javie możemy tworzyć wątki na cztery główne sposoby, z których każdy ma swoje zastosowanie:

// Sposób 1: extends Thread (nie zalecany)
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread: " + getName());
    }
}
new MyThread().start();

// Sposób 2: implements Runnable (zalecany)
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable");
    }
}
new Thread(new MyRunnable()).start();

// Sposób 3: Lambda (najczęstszy od Java 8)
new Thread(() -> System.out.println("Lambda")).start();

// Sposób 4: Callable (zwraca wartość)
Callable<Integer> callable = () -> {
    return 42;
};
Future<Integer> future = executor.submit(callable);

Dlaczego Runnable lepszy:

  1. Java nie wspiera wielodziedziczenia - klasa może rozszerzać tylko jedną klasę
  2. Separacja logiki (Runnable) od mechanizmu (Thread)
  3. Możliwość użycia z ExecutorService
  4. Łatwiejsze testowanie

Czym różni się start() od run()?

Odpowiedź w 30 sekund:

  • start() - tworzy nowy wątek OS i wywołuje run() w tym wątku
  • run() - zwykła metoda, wykonuje się w bieżącym wątku

Wywołanie run() bezpośrednio to częsty błąd - nie tworzy nowego wątku!

Odpowiedź w 2 minuty:

Poniższy przykład pokazuje różnicę w zachowaniu tych dwóch metod:

Thread t = new Thread(() -> {
    System.out.println("Thread: " + Thread.currentThread().getName());
});

// ❌ Błąd - wykonuje się w main thread
t.run();  // Output: Thread: main

// ✅ Poprawnie - nowy wątek
t.start();  // Output: Thread: Thread-0

Co robi start():

  1. Alokuje zasoby dla nowego wątku (stack, rejestracja w OS)
  2. Zmienia stan wątku na RUNNABLE
  3. Wywołuje run() w nowym wątku
  4. Zwraca natychmiast (nie czeka na zakończenie)
// start() można wywołać tylko raz!
Thread t = new Thread(() -> {});
t.start();
t.start();  // IllegalThreadStateException!

Jakie są stany wątku w Javie?

Odpowiedź w 30 sekund:

NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED

Odpowiedź w 2 minuty:

Wątek przechodzi przez różne stany w trakcie swojego życia. Metoda getState() pozwala sprawdzić bieżący stan, co jest przydatne przy debugowaniu problemów z wielowątkowością.

Thread.State state = thread.getState();
Stan Opis Przejście
NEW Utworzony, nie uruchomiony new Thread()
RUNNABLE Gotowy lub wykonujący się start()
BLOCKED Czeka na monitor (synchronized) Inny wątek trzyma lock
WAITING Czeka bez timeout wait(), join(), park()
TIMED_WAITING Czeka z timeout sleep(), wait(timeout)
TERMINATED Zakończony run() się skończyło
Thread t = new Thread(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {}
});

System.out.println(t.getState());  // NEW
t.start();
System.out.println(t.getState());  // RUNNABLE
Thread.sleep(100);
System.out.println(t.getState());  // TIMED_WAITING
t.join();
System.out.println(t.getState());  // TERMINATED

Jak zatrzymać wątek?

Odpowiedź w 30 sekund:

  • Nigdy nie używaj stop() (deprecated, niebezpieczne)
  • Użyj flagi volatile boolean + sprawdzaj w pętli
  • Lub interrupt() + obsłuż InterruptedException

Odpowiedź w 2 minuty:

Istnieją dwa bezpieczne sposoby zatrzymywania wątków - flaga volatile lub mechanizm interrupt():

// ❌ Nigdy! Może zostawić obiekt w niespójnym stanie
thread.stop();

// ✅ Sposób 1: Flaga volatile
class Task implements Runnable {
    private volatile boolean running = true;

    public void run() {
        while (running) {
            // praca
        }
    }

    public void stop() {
        running = false;
    }
}

// ✅ Sposób 2: interrupt() - preferowany
class Task implements Runnable {
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();  // przywróć flagę
                break;
            }
        }
    }
}

Thread t = new Thread(new Task());
t.start();
t.interrupt();  // ustaw flagę interrupted

Dlaczego interrupt() lepszy:

  • Działa z metodami blokującymi (sleep, wait, join)
  • Standardowy mechanizm Javy
  • Thread pool używa interrupt do zatrzymywania

Synchronizacja

Jak działa synchronized?

Odpowiedź w 30 sekund:

synchronized zapewnia:

  1. Mutual exclusion - tylko jeden wątek naraz
  2. Visibility - zmiany widoczne dla innych wątków
  3. Happens-before - uporządkowanie operacji

Blokuje na monitorze obiektu.

Odpowiedź w 2 minuty:

Synchronized można używać zarówno jako modyfikator metody, jak i jako blok na wybranym obiekcie:

// Synchronized metoda - blokuje na 'this'
public synchronized void increment() {
    count++;
}

// Synchronized blok - blokuje na podanym obiekcie
public void increment() {
    synchronized (this) {
        count++;
    }
}

// Synchronized na innym obiekcie (lepsze)
private final Object lock = new Object();

public void increment() {
    synchronized (lock) {
        count++;
    }
}

// Synchronized static - blokuje na Class
public static synchronized void staticMethod() {
    // blokuje na MyClass.class
}

Jak działa pod spodem:

Thread A: monitorenter(obj) → wykonaj kod → monitorexit(obj)
Thread B: monitorenter(obj) → BLOCKED (czeka) → ...

Najlepsze praktyki:

  1. Preferuj synchronized bloki nad metody (mniejszy scope)
  2. Używaj prywatnego obiektu jako lock (nie this)
  3. Minimalizuj czas trzymania locka
  4. Rozważ java.util.concurrent zamiast synchronized

Czym różni się synchronized od ReentrantLock?

Odpowiedź w 30 sekund:

Cecha synchronized ReentrantLock
Składnia Wbudowane keyword Jawny API
Try lock Nie Tak (tryLock)
Timeout Nie Tak
Fairness Nie Opcjonalnie
Interruptible Nie Tak

Odpowiedź w 2 minuty:

Główna różnica polega na sposobie zarządzania lockiem i dostępnych funkcjonalnościach:

// synchronized - automatyczny unlock
synchronized (lock) {
    // kod
}  // auto-unlock

// ReentrantLock - manualny unlock (MUSI być w finally!)
Lock lock = new ReentrantLock();

lock.lock();
try {
    // kod
} finally {
    lock.unlock();  // ZAWSZE w finally!
}

Zaawansowane możliwości ReentrantLock:

// Try lock - nie czeka
if (lock.tryLock()) {
    try {
        // kod
    } finally {
        lock.unlock();
    }
} else {
    // lock zajęty, zrób coś innego
}

// Try lock z timeout
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    // ...
}

// Interruptible lock
try {
    lock.lockInterruptibly();
    // ...
} catch (InterruptedException e) {
    // wątek został przerwany podczas czekania
}

// Fair lock (FIFO)
Lock fairLock = new ReentrantLock(true);

Kiedy ReentrantLock:

  • Potrzebujesz tryLock lub timeout
  • Chcesz fair scheduling
  • Potrzebujesz wielu Condition

Co to jest volatile i kiedy go używać?

Odpowiedź w 30 sekund:

volatile zapewnia:

  1. Visibility - zapis widoczny natychmiast dla innych wątków
  2. Happens-before - zapis przed odczytem

NIE zapewnia atomowości złożonych operacji (i++ nie jest atomowe)!

Odpowiedź w 2 minuty:

Bez volatile flaga może być cache'owana w rejestrze CPU i zmiany nie będą widoczne między wątkami:

// Problem bez volatile
class Stopper {
    private boolean stopped = false;  // może być cache'owane w rejestrze

    public void stop() { stopped = true; }

    public void run() {
        while (!stopped) {  // może nigdy nie zobaczyć zmiany!
            // praca
        }
    }
}

// ✅ Z volatile
class Stopper {
    private volatile boolean stopped = false;

    public void stop() { stopped = true; }  // zapis do głównej pamięci

    public void run() {
        while (!stopped) {  // zawsze czyta z głównej pamięci
            // praca
        }
    }
}

Kiedy volatile NIE wystarczy:

private volatile int count = 0;

// ❌ NIE jest thread-safe!
public void increment() {
    count++;  // read → modify → write (3 operacje!)
}

// ✅ Użyj AtomicInteger
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
    count.incrementAndGet();  // atomowe
}

Użyj volatile dla:

  • Prostych flag (stop, initialized)
  • Obiektów immutable (gdy zmienia się referencja)
  • Double-checked locking pattern

Co to jest deadlock i jak go uniknąć?

Odpowiedź w 30 sekund:

Deadlock = wątki czekają na siebie nawzajem w cyklu.

Warunki (wszystkie muszą być spełnione):

  1. Mutual exclusion
  2. Hold and wait
  3. No preemption
  4. Circular wait

Odpowiedź w 2 minuty:

Poniższy przykład pokazuje klasyczną sytuację deadlocka oraz sposoby jego unikania:

// Klasyczny deadlock
Object lockA = new Object();
Object lockB = new Object();

// Thread 1
synchronized (lockA) {
    Thread.sleep(100);
    synchronized (lockB) {  // czeka na Thread 2
        // ...
    }
}

// Thread 2
synchronized (lockB) {
    Thread.sleep(100);
    synchronized (lockA) {  // czeka na Thread 1
        // ...
    }
}
// DEADLOCK!

Rozwiązania:

// 1. Stała kolejność locków
// Zawsze lockA przed lockB
synchronized (lockA) {
    synchronized (lockB) { }
}

// 2. tryLock z timeout
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
    try {
        if (lockB.tryLock(1, TimeUnit.SECONDS)) {
            try {
                // praca
            } finally { lockB.unlock(); }
        }
    } finally { lockA.unlock(); }
}

// 3. Lock ordering przez hash
int hashA = System.identityHashCode(lockA);
int hashB = System.identityHashCode(lockB);
Object first = hashA < hashB ? lockA : lockB;
Object second = hashA < hashB ? lockB : lockA;
synchronized (first) {
    synchronized (second) { }
}

Detekcja: jstack <pid> pokaże deadlocked threads.


Czym różni się wait() od sleep()?

Odpowiedź w 30 sekund:

Cecha wait() sleep()
Klasa Object Thread
Wymaga synchronized Nie
Zwalnia lock Tak Nie
Budzenie notify()/notifyAll() Timeout

Odpowiedź w 2 minuty:

Metoda wait() zwalnia lock i pozwala innym wątkom wejść do synchronized bloku, podczas gdy sleep() blokuje wątek trzymając lock:

// wait() - zwalnia lock i czeka na notify
synchronized (lock) {
    while (!condition) {
        lock.wait();  // zwalnia lock!
    }
    // condition spełniony
}

// Inny wątek:
synchronized (lock) {
    condition = true;
    lock.notify();  // budzi jeden czekający wątek
    // lub lock.notifyAll();  // budzi wszystkie
}

// sleep() - nie zwalnia locka!
synchronized (lock) {
    Thread.sleep(1000);  // trzyma lock przez 1 sekundę!
}

Producer-Consumer pattern:

class Buffer {
    private Queue<Integer> queue = new LinkedList<>();
    private final int MAX = 10;

    public synchronized void put(int val) throws InterruptedException {
        while (queue.size() == MAX) {
            wait();  // czekaj aż konsument zabierze
        }
        queue.add(val);
        notifyAll();  // obudź konsumentów
    }

    public synchronized int take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();  // czekaj aż producent doda
        }
        int val = queue.poll();
        notifyAll();  // obudź producentów
        return val;
    }
}

Zawsze używaj wait() w pętli while, nie if! (spurious wakeups)


ExecutorService i Thread Pools

Dlaczego używać ExecutorService zamiast new Thread()?

Odpowiedź w 30 sekund:

  1. Reużycie wątków - brak overhead tworzenia
  2. Kontrola zasobów - limit wątków
  3. Kolejkowanie - zadania czekają gdy wszystkie wątki zajęte
  4. Lifecycle management - graceful shutdown

Odpowiedź w 2 minuty:

ExecutorService pozwala na efektywne zarządzanie pulą wątków zamiast tworzenia nowych dla każdego zadania:

// ❌ Nie skaluje się
for (int i = 0; i < 1000; i++) {
    new Thread(() -> doWork()).start();
}
// 1000 wątków! Overhead OS, memory, context switching

// ✅ Thread pool
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> doWork());
}
executor.shutdown();
// Tylko 10 wątków, 990 zadań w kolejce

Typy pul:

// Fixed - stała liczba wątków
ExecutorService fixed = Executors.newFixedThreadPool(4);

// Cached - tworzy wątki w razie potrzeby, reużywa
ExecutorService cached = Executors.newCachedThreadPool();

// Single - jeden wątek, sekwencyjne zadania
ExecutorService single = Executors.newSingleThreadExecutor();

// Scheduled - opóźnienia i cykliczne zadania
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
scheduled.scheduleAtFixedRate(() -> {}, 0, 1, TimeUnit.SECONDS);

Graceful shutdown:

executor.shutdown();  // nie przyjmuj nowych, dokończ bieżące
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();  // przerwij wszystko
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

Czym różni się submit() od execute()?

Odpowiedź w 30 sekund:

Cecha execute() submit()
Zwraca void Future
Wyjątki UncaughtExceptionHandler Future.get() rzuca
Interface Executor ExecutorService

Odpowiedź w 2 minuty:

Metoda execute() nie zwraca wyniku, podczas gdy submit() zwraca Future do śledzenia statusu i wyniku:

ExecutorService executor = Executors.newFixedThreadPool(4);

// execute() - fire-and-forget
executor.execute(() -> {
    throw new RuntimeException("Oops");
    // wyjątek leci do UncaughtExceptionHandler
});

// submit() - z Future
Future<?> future = executor.submit(() -> {
    throw new RuntimeException("Oops");
});

try {
    future.get();  // blokuje, rzuca ExecutionException
} catch (ExecutionException e) {
    Throwable cause = e.getCause();  // oryginalny wyjątek
}

// submit() z wartością zwrotną
Future<Integer> result = executor.submit(() -> {
    return 42;
});
Integer value = result.get();  // 42

Future API:

Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "Done";
});

future.isDone();      // czy zakończone
future.isCancelled(); // czy anulowane
future.cancel(true);  // anuluj (true = interrupt)
future.get();         // blokuje do wyniku
future.get(1, TimeUnit.SECONDS);  // z timeout

Jak skonfigurować ThreadPoolExecutor?

Odpowiedź w 30 sekund:

new ThreadPoolExecutor(
    corePoolSize,     // minimalna liczba wątków
    maxPoolSize,      // maksymalna liczba wątków
    keepAliveTime,    // czas życia idle wątków
    unit,             // jednostka czasu
    workQueue,        // kolejka zadań
    threadFactory,    // tworzenie wątków
    handler           // obsługa odrzuconych
);

Odpowiedź w 2 minuty:

ThreadPoolExecutor oferuje pełną kontrolę nad wszystkimi parametrami puli wątków:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                              // core: 4 wątki zawsze
    10,                             // max: do 10 gdy kolejka pełna
    60L, TimeUnit.SECONDS,          // idle wątki (>core) żyją 60s
    new ArrayBlockingQueue<>(100),  // kolejka 100 zadań
    new ThreadFactoryBuilder()
        .setNameFormat("worker-%d")
        .build(),
    new ThreadPoolExecutor.CallerRunsPolicy()  // gdy przepełnienie
);

Strategie odrzucania:

Policy Zachowanie
AbortPolicy RejectedExecutionException (domyślna)
CallerRunsPolicy Caller wykonuje zadanie
DiscardPolicy Cicho odrzuca
DiscardOldestPolicy Usuwa najstarsze z kolejki

Sizing thread pool:

  • CPU-bound: N threads ≈ N cores
  • I/O-bound: N threads ≈ N cores × (1 + wait_time/compute_time)
int cores = Runtime.getRuntime().availableProcessors();
// CPU-bound
ExecutorService cpu = Executors.newFixedThreadPool(cores);
// I/O-bound (np. DB, HTTP) - więcej wątków
ExecutorService io = Executors.newFixedThreadPool(cores * 2);

CompletableFuture

Co to jest CompletableFuture i jak go używać?

Odpowiedź w 30 sekund:

CompletableFuture = Future + callback API. Pozwala na:

  • Łączenie asynchronicznych operacji
  • Transformacje wyników (map/flatMap style)
  • Obsługę błędów
  • Kombinowanie wielu futures

Odpowiedź w 2 minuty:

CompletableFuture umożliwia budowanie łańcuchów asynchronicznych operacji z wykorzystaniem metod callback:

// Tworzenie
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return fetchData();  // wykonuje się w ForkJoinPool
});

// Transformacja (thenApply = map)
CompletableFuture<Integer> length = future.thenApply(String::length);

// Konsumpcja (thenAccept)
future.thenAccept(System.out::println);

// Łączenie (thenCompose = flatMap)
CompletableFuture<String> result = future.thenCompose(data -> {
    return CompletableFuture.supplyAsync(() -> process(data));
});

// Obsługa błędów
future.exceptionally(ex -> "default")
      .thenAccept(System.out::println);

// Handle (sukces i błąd)
future.handle((result, ex) -> {
    if (ex != null) return "error";
    return result;
});

Async variants:

// Synchroniczne (w tym samym wątku co poprzednia operacja)
future.thenApply(s -> s.toUpperCase());

// Asynchroniczne (w nowym wątku)
future.thenApplyAsync(s -> s.toUpperCase());

// Asynchroniczne z własnym executorem
future.thenApplyAsync(s -> s.toUpperCase(), myExecutor);

Jak łączyć wiele CompletableFuture?

Odpowiedź w 30 sekund:

  • thenCombine() - łączy 2 futures
  • allOf() - czeka na wszystkie
  • anyOf() - czeka na pierwszy

Odpowiedź w 2 minuty:

Do łączenia wielu CompletableFuture służą metody combine, allOf i anyOf:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

// Combine - łączy wyniki dwóch
CompletableFuture<String> combined = future1.thenCombine(future2,
    (s1, s2) -> s1 + " " + s2);  // "Hello World"

// AllOf - czeka na wszystkie
List<CompletableFuture<String>> futures = List.of(future1, future2);

CompletableFuture<Void> all = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0])
);

// Zbierz wyniki
CompletableFuture<List<String>> results = all.thenApply(v ->
    futures.stream()
           .map(CompletableFuture::join)
           .toList()
);

// AnyOf - pierwszy który się skończy
CompletableFuture<Object> any = CompletableFuture.anyOf(
    future1, future2
);
String first = (String) any.get();

Praktyczny przykład - równoległe API calls:

public CompletableFuture<UserProfile> getUserProfile(String userId) {
    CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> userService.getUser(userId));

    CompletableFuture<List<Order>> ordersFuture =
        CompletableFuture.supplyAsync(() -> orderService.getOrders(userId));

    CompletableFuture<Settings> settingsFuture =
        CompletableFuture.supplyAsync(() -> settingsService.getSettings(userId));

    return userFuture.thenCombine(ordersFuture, (user, orders) ->
        new UserWithOrders(user, orders)
    ).thenCombine(settingsFuture, (userWithOrders, settings) ->
        new UserProfile(userWithOrders, settings)
    );
}

Atomic Classes i Lock-Free

Czym są klasy Atomic i kiedy ich używać?

Odpowiedź w 30 sekund:

Atomic classes (AtomicInteger, AtomicLong, AtomicReference) zapewniają atomowe operacje bez synchronized. Używają CAS (Compare-And-Swap) - lock-free, szybsze przy niskiej konkurencji.

Odpowiedź w 2 minuty:

Klasy Atomic oferują alternatywę dla synchronized przy prostych operacjach na pojedynczych zmiennych:

// ❌ Nie thread-safe
private int counter = 0;
public void increment() { counter++; }  // read-modify-write

// ✅ Z synchronized
private int counter = 0;
public synchronized void increment() { counter++; }

// ✅ Z AtomicInteger (szybsze)
private AtomicInteger counter = new AtomicInteger(0);
public void increment() { counter.incrementAndGet(); }

Atomic API:

AtomicInteger ai = new AtomicInteger(0);

ai.get();                    // odczyt
ai.set(10);                  // zapis
ai.incrementAndGet();        // ++i
ai.getAndIncrement();        // i++
ai.addAndGet(5);             // += 5
ai.compareAndSet(10, 20);    // if (val == 10) val = 20

// Od Java 8 - update function
ai.updateAndGet(x -> x * 2);        // podwój wartość
ai.accumulateAndGet(5, (x, y) -> x + y);  // dodaj 5

AtomicReference:

AtomicReference<User> userRef = new AtomicReference<>(initialUser);

// Atomowa zamiana
userRef.set(newUser);

// Compare-and-swap
User expected = userRef.get();
User updated = new User(expected.name(), newEmail);
userRef.compareAndSet(expected, updated);

// Update function
userRef.updateAndGet(user -> user.withEmail(newEmail));

Jak działa CAS (Compare-And-Swap)?

Odpowiedź w 30 sekund:

CAS to atomowa operacja CPU: "jeśli wartość == expected, ustaw na new". Jeśli nie - zwróć false i spróbuj ponownie. Lock-free, ale może powodować spinning przy wysokiej konkurencji.

Odpowiedź w 2 minuty:

Mechanizm CAS działa jako atomowa operacja na poziomie CPU - sprawdza i zamienia wartość w jednej niepodzielnej instrukcji:

CAS(address, expected, new):
  atomically {
    if (*address == expected) {
      *address = new
      return true
    } else {
      return false
    }
  }

Implementacja AtomicInteger:

// Pseudokod increment()
public int incrementAndGet() {
    while (true) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next)) {
            return next;
        }
        // CAS failed, retry (inny wątek zmienił wartość)
    }
}

CAS vs Lock:

Cecha CAS Lock (synchronized)
Blokowanie Nie Tak
Spinning Tak Nie
Niska konkurencja Szybszy Wolniejszy
Wysoka konkurencja Spinning overhead Lepszy

Problem ABA:

// Wątek 1: read A
// Wątek 2: change A → B → A
// Wątek 1: CAS succeeds (widzi A), ale stan się zmienił!

// Rozwiązanie: AtomicStampedReference
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String val = ref.get(stampHolder);
ref.compareAndSet(val, "B", stampHolder[0], stampHolder[0] + 1);

ConcurrentCollections

Czym różni się ConcurrentHashMap od synchronizedMap?

Odpowiedź w 30 sekund:

Cecha synchronizedMap ConcurrentHashMap
Locking Cała mapa Per-segment/bucket
Iteracja Wymaga locka Lock-free (weakly consistent)
null Dozwolony Nie
Wydajność Słaba przy wielu wątkach Skaluje się

Odpowiedź w 2 minuty:

ConcurrentHashMap wykorzystuje zaawansowane techniki blokowania per-segment, co zapewnia lepszą wydajność:

// synchronizedMap - jeden globalny lock
Map<K, V> syncMap = Collections.synchronizedMap(new HashMap<>());
synchronized (syncMap) {  // trzeba ręcznie przy iteracji!
    for (Entry<K, V> e : syncMap.entrySet()) { }
}

// ConcurrentHashMap - lock-free reads, segmented writes
ConcurrentMap<K, V> concMap = new ConcurrentHashMap<>();
for (Entry<K, V> e : concMap.entrySet()) { }  // bezpieczne

ConcurrentHashMap - atomowe operacje:

ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();

// Atomowe put-if-absent
map.putIfAbsent("key", 0);

// Atomowy compute
map.compute("key", (k, v) -> v == null ? 1 : v + 1);

// Atomowy merge
map.merge("key", 1, Integer::sum);  // dodaj lub zwiększ

// forEach, reduce, search - równoległe!
map.forEach(4, (k, v) -> System.out.println(k + "=" + v));
long sum = map.reduceValuesToLong(4, v -> v, 0, Long::sum);

Jakie są inne concurrent collections?

Odpowiedź w 30 sekund:

  • ConcurrentLinkedQueue - lock-free FIFO
  • CopyOnWriteArrayList - read-optimized list
  • BlockingQueue - producer-consumer (LinkedBlockingQueue, ArrayBlockingQueue)
  • ConcurrentSkipListMap - sorted concurrent map

Odpowiedź w 2 minuty:

Java oferuje różne concurrent collections dostosowane do specyficznych przypadków użycia:

// BlockingQueue - producer-consumer
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);

// Producer
queue.put(task);  // blokuje gdy pełna

// Consumer
Task task = queue.take();  // blokuje gdy pusta

// CopyOnWriteArrayList - read-optimized
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("item");  // kopiuje całą tablicę! Wolne.
for (String s : list) { }  // szybkie, brak locków

// Use case: niewiele writes, dużo reads (np. lista listeners)

// ConcurrentLinkedQueue - non-blocking FIFO
ConcurrentLinkedQueue<Event> events = new ConcurrentLinkedQueue<>();
events.offer(event);
Event e = events.poll();  // null jeśli pusta

// ConcurrentSkipListMap - sorted, concurrent
ConcurrentNavigableMap<Integer, String> sortedMap =
    new ConcurrentSkipListMap<>();
sortedMap.put(3, "three");
sortedMap.put(1, "one");
sortedMap.headMap(2);  // {1=one}

Java Memory Model

Co to jest happens-before?

Odpowiedź w 30 sekund:

Happens-before to relacja gwarantująca, że efekty jednej operacji są widoczne dla drugiej. Bez happens-before, kompilator/CPU może reorderować operacje.

Odpowiedź w 2 minuty:

Java Memory Model definiuje sześć podstawowych reguł happens-before gwarantujących widoczność zmian między wątkami:

Reguły happens-before:

  1. Program order - operacje w jednym wątku są uporządkowane
  2. Monitor lock - unlock happens-before następny lock
  3. Volatile - write happens-before read
  4. Thread start - start() happens-before run()
  5. Thread join - operacje w wątku happens-before join()
  6. Transitivity - A hb B, B hb C → A hb C
// Bez happens-before - BŁĄD!
class Broken {
    int x = 0;
    boolean ready = false;

    // Thread 1
    void writer() {
        x = 42;
        ready = true;  // może być reordered przed x=42!
    }

    // Thread 2
    void reader() {
        if (ready) {
            System.out.println(x);  // może wydrukować 0!
        }
    }
}

// Z volatile - happens-before
class Fixed {
    int x = 0;
    volatile boolean ready = false;  // volatile!

    void writer() {
        x = 42;           // (1)
        ready = true;      // (2) volatile write
        // (1) happens-before (2)
    }

    void reader() {
        if (ready) {       // (3) volatile read, (2) hb (3)
            System.out.println(x);  // widzi 42, (1) hb (3)
        }
    }
}

Co to jest double-checked locking i jak go poprawnie zaimplementować?

Odpowiedź w 30 sekund:

Double-checked locking = sprawdź → zablokuj → sprawdź ponownie. Wymaga volatile, bez tego jest broken (reordering).

Odpowiedź w 2 minuty:

Implementacja double-checked locking wymaga słowa kluczowego volatile, aby zapobiec problemom z reorderingiem:

// ❌ BROKEN - bez volatile
class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {           // (1) check
            synchronized (Singleton.class) {
                if (instance == null) {   // (2) recheck
                    instance = new Singleton();  // (3) create
                    // Problem: (3) może być reordered!
                    // Inny wątek może zobaczyć partially constructed object
                }
            }
        }
        return instance;
    }
}

// ✅ POPRAWNIE - z volatile
class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                    // volatile write happens-after konstrukcja
                }
            }
        }
        return instance;  // volatile read
    }
}

// ✅ NAJLEPIEJ - Holder idiom (lazy, thread-safe)
class Singleton {
    private Singleton() {}

    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

Zobacz też


Ten artykuł jest częścią serii przygotowującej do rozmów rekrutacyjnych na stanowisko Java Backend Developer. Sprawdź nasze fiszki z pytaniami rekrutacyjnymi: Java.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.