Java Concurrency - Pytania Rekrutacyjne dla Senior Developer [2026]
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:
- Java nie wspiera wielodziedziczenia - klasa może rozszerzać tylko jedną klasę
- Separacja logiki (Runnable) od mechanizmu (Thread)
- Możliwość użycia z ExecutorService
- Łatwiejsze testowanie
Czym różni się start() od run()?
Odpowiedź w 30 sekund:
-
start()- tworzy nowy wątek OS i wywołujerun()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():
- Alokuje zasoby dla nowego wątku (stack, rejestracja w OS)
- Zmienia stan wątku na RUNNABLE
- Wywołuje
run()w nowym wątku - 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:
- Mutual exclusion - tylko jeden wątek naraz
- Visibility - zmiany widoczne dla innych wątków
- 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:
- Preferuj synchronized bloki nad metody (mniejszy scope)
- Używaj prywatnego obiektu jako lock (nie
this) - Minimalizuj czas trzymania locka
- Rozważ
java.util.concurrentzamiast 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:
- Visibility - zapis widoczny natychmiast dla innych wątków
- 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):
- Mutual exclusion
- Hold and wait
- No preemption
- 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:
- Reużycie wątków - brak overhead tworzenia
- Kontrola zasobów - limit wątków
- Kolejkowanie - zadania czekają gdy wszystkie wątki zajęte
- 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:
- Program order - operacje w jednym wątku są uporządkowane
- Monitor lock - unlock happens-before następny lock
- Volatile - write happens-before read
- Thread start - start() happens-before run()
- Thread join - operacje w wątku happens-before join()
- 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ż
- Kompletny Przewodnik - Rozmowa Java Backend Developer - pełny przewodnik przygotowania do rozmowy
- Java - Pytania Rekrutacyjne - 150 pytań Java Core
- Spring Boot - Pytania Rekrutacyjne - 56 pytań Spring Framework
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.
