Java – Quiz: Multithreading & ConcurrencyMultithreading and concurrency are core topics for any Java developer aiming to build responsive, high-performance applications. This article provides a comprehensive guide to the key concepts, pitfalls, and best practices you need to master for quizzes, interviews, and real-world coding. It includes explanations, code examples, common quiz questions with answers, and practical tips to avoid concurrency bugs.
Why multithreading matters in Java
Java applications often need to perform multiple tasks at the same time: handling user input while processing data, serving many clients concurrently in a server, or utilizing multiple CPU cores for computation. Multithreading lets a program execute multiple threads of control within a single process. Concurrency is the broader concept that deals with managing multiple tasks that make progress independently, whether in parallel (multiple CPU cores) or interleaved on a single core.
Key benefits
- Responsiveness — UI or server remains responsive while work continues.
- Throughput — More work can be completed by utilizing multiple cores.
- Resource utilization — Overlap I/O-bound tasks while CPUs wait.
Thread basics
A thread is the smallest unit of execution. Java provides threads via the java.lang.Thread class and the Runnable and Callable interfaces.
Example: creating and starting a thread
class MyTask implements Runnable { public void run() { System.out.println("Task running on " + Thread.currentThread().getName()); } } Thread t = new Thread(new MyTask(), "worker-1"); t.start();
Callable vs Runnable:
- Runnable: run() returns void and cannot throw checked exceptions.
- Callable
: call() returns a value and may throw exceptions; works with Future to retrieve results.
Creating threads via ExecutorService (preferred)
ExecutorService exec = Executors.newFixedThreadPool(4); Future<Integer> f = exec.submit(() -> { // compute and return result return 42; }); int result = f.get(); exec.shutdown();
Memory model, visibility, and happens-before
Java Memory Model (JMM) defines how threads interact through memory and what guarantees the language provides regarding visibility and ordering.
- Volatile: ensures visibility of changes across threads and prevents certain reorderings for that variable.
- volatile guarantees that a read of the variable sees the most recent write.
- Final fields: properly constructed objects with final fields have stronger guarantees for visibility after construction.
- Synchronization (synchronized blocks/methods) provides mutual exclusion and establishes happens-before relationships:
- An unlock on a monitor happens-before every subsequent lock on that monitor.
Common pitfalls:
- Without proper synchronization, writes by one thread may not be visible to others.
- Double-checked locking requires volatile for the reference variable to be correct.
Example: double-checked locking correctly
public class Singleton { private static volatile Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
Locks, synchronized, and ReentrantLock
Synchronized:
- Simpler, JVM-managed monitor lock on objects or classes.
- Reentrant: the same thread can acquire the lock multiple times.
ReentrantLock (java.util.concurrent.locks.ReentrantLock):
- More features: tryLock with timeout, interruptible lock acquisition, fairness options, Condition objects for signaling.
- Use when you need advanced capabilities beyond synchronized.
Example: using ReentrantLock
ReentrantLock lock = new ReentrantLock(); try { if (lock.tryLock(1, TimeUnit.SECONDS)) { try { // guarded code } finally { lock.unlock(); } } else { // handle lock not acquired } } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
Concurrent collections and utilities
Java provides thread-safe collections and utilities in java.util.concurrent to simplify concurrency.
Common classes:
- ConcurrentHashMap — scalable concurrent hash table.
- CopyOnWriteArrayList — good for lists with infrequent writes and many reads.
- BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue) — useful for producer-consumer patterns.
- ConcurrentLinkedQueue — non-blocking queue.
- ThreadPoolExecutor — advanced thread-pool customization.
- ForkJoinPool — efficient for divide-and-conquer tasks, works with RecursiveTask/RecursiveAction.
Example: producer-consumer using BlockingQueue
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100); Runnable producer = () -> { try { queue.put("item"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; Runnable consumer = () -> { try { String item = queue.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } };
Atomic variables and non-blocking algorithms
Atomic classes (java.util.concurrent.atomic) provide lock-free thread-safe operations on single variables:
- AtomicInteger, AtomicReference, AtomicLong, AtomicBoolean, and field updater variants.
Compare-and-set (CAS) is the foundation for these classes. They avoid blocking, reduce context switches, and can improve throughput in high-concurrency scenarios.
Example: AtomicInteger counter
AtomicInteger counter = new AtomicInteger(0); int newVal = counter.incrementAndGet();
Be careful: atomic operations are for single variables — complex invariants across multiple variables may still require locks or other coordination.
Thread coordination: wait/notify, CountDownLatch, CyclicBarrier, Phaser
Primitive wait/notify:
- Works with synchronized monitors to wait and notify threads.
- Prone to missed signals and must be used carefully.
Higher-level utilities:
- CountDownLatch — one-time gate that blocks until count reaches zero.
- CyclicBarrier — barrier that waits for a fixed number of threads to reach a point, reusable.
- Phaser — flexible barrier that supports dynamic registration and phased execution.
- Semaphore — controls a set of permits for resource access.
Example: CountDownLatch
CountDownLatch latch = new CountDownLatch(3); Runnable worker = () -> { // do work latch.countDown(); }; latch.await(); // wait until all workers finish
Fork/Join framework and parallel streams
ForkJoinPool is optimized for tasks that can be recursively split into subtasks (work-stealing algorithm). Use RecursiveTask
Example: ForkJoin sum
class SumTask extends RecursiveTask<Long> { private final long[] arr; int lo, hi; // compute splitting threshold, fork/join logic } ForkJoinPool pool = new ForkJoinPool(); long result = pool.invoke(new SumTask(arr, 0, arr.length));
Parallel streams (Stream.parallel()) use the common ForkJoinPool by default. They provide an easy way to parallelize data processing but require care:
- Ensure operations are stateless and thread-safe.
- Beware of shared mutable state and non-associative reduction operations.
Deadlocks, livelocks, and starvation
Deadlock: two or more threads waiting indefinitely for locks held by each other. Avoid by:
- Lock ordering — acquire locks in a consistent global order.
- TryLock with timeout — fail gracefully if lock cannot be obtained.
- Reducing lock granularity or using lock-free structures.
Livelock: threads keep reacting to each other and making no progress (like two people stepping aside repeatedly). Usually fixed by introducing randomness or back-off strategies.
Starvation: a thread never gets CPU or lock time due to scheduling or fairness. Use fair locks carefully (they can reduce throughput).
Common concurrency mistakes and how quizzes test them
- Missing volatile or synchronization resulting in stale reads.
- Incorrect double-checked locking without volatile.
- Assuming incrementing a non-atomic int is thread-safe.
- Using non-thread-safe collections (ArrayList, HashMap) in concurrent contexts.
- Blocking on UI thread in desktop/web frameworks.
- Misunderstanding thread lifecycle and forgetting to shutdown ExecutorService.
Typical quiz questions:
- What does volatile guarantee?
- Answer: Visibility of writes to the volatile variable across threads and prevents certain reorderings for that variable.
- Difference between synchronized and ReentrantLock?
- Answer: synchronized is simpler JVM-managed monitor; ReentrantLock offers tryLock, interruptible waits, fairness, and Condition objects.
- How to safely publish an object?
- Answer: Through final fields, volatile reference, or proper synchronization (happens-before rules).
- What is a race condition?
- Answer: A bug where correctness depends on the unpredictable timing or interleaving of threads.
- When to use ConcurrentHashMap vs Collections.synchronizedMap?
- Answer: ConcurrentHashMap provides better scalability and concurrent read/update performance without locking entire map.
Quiz-style practice set (with brief answers)
-
What happens if you call Thread.start() twice on the same Thread object?
- Throws IllegalThreadStateException.
-
Which method waits for a thread to finish?
- Thread.join().
-
Is Stream.parallel() always faster than a sequential stream?
- No — it depends on workload, data size, and thread overhead.
-
How do you interrupt a blocking thread stuck on Thread.sleep()?
- Call thread.interrupt(); the thread receives InterruptedException.
-
Explain compareAndSet in AtomicInteger.
- Atomically sets the value to update if current value equals expected; returns boolean success.
-
How to avoid ConcurrentModificationException when iterating and modifying a collection?
- Use concurrent collections (e.g., ConcurrentHashMap) or iterator.remove() where supported, or copy before iteration.
-
What is the default behavior of ForkJoinPool.commonPool() threads (daemon or user)?
- They are daemon threads.
Best practices and practical tips
- Prefer higher-level concurrency utilities (ExecutorService, concurrent collections) over manually managing Thread objects.
- Minimize shared mutable state; prefer immutability and pure functions when possible.
- Use ExecutorService and always shutdown() or shutdownNow() to avoid resource leaks.
- Keep synchronized blocks small and avoid holding locks while performing long-running operations or I/O.
- Use timeouts (tryLock, await with timeout) to avoid permanent waits.
- Write tests that reproduce concurrency issues using tools like jcstress or stress tests.
- Use thread dumps and profilers to diagnose deadlocks and contention (jstack, jvisualvm).
Summary
Multithreading and concurrency in Java are powerful but complex. Understanding the Java Memory Model, synchronization primitives, concurrent collections, and higher-level utilities will help you write correct and performant concurrent code. Practice with quiz-style questions and real code—especially using ExecutorService, atomic classes, and lock-free structures—until these concepts become second nature.
Leave a Reply