Interview contents
Advanced Topics & Design Patterns in Java Concurrency
In this post, we’ll cover:
- Lock-free algorithms and Compare-And-Swap (CAS)
- Designing thread-safe classes (immutability, confinement, composition)
- Java concurrency utilities (CountDownLatch, CyclicBarrier, ExecutorService)
- Testing and detecting race conditions
- Real-world design examples
- Common interview questions and best ways to answer them
🧮 1. Lock-Free Algorithms and Compare-And-Swap (CAS)
The Need for Lock-Free Designs
Traditional synchronization (synchronized, ReentrantLock) ensures mutual exclusion but comes at a cost:
- Context switching overhead
- Potential deadlocks
- Reduced scalability under contention
Lock-free algorithms, in contrast, rely on atomic hardware operations to achieve thread safety without blocking.
What Is Compare-And-Swap (CAS)?
At the heart of lock-free programming lies the Compare-And-Swap instruction — a CPU-level atomic primitive. It compares a variable’s current value to an expected value and, if they match, replaces it with a new value.
boolean compareAndSwapInt(Object obj, long offset, int expect, int update);Conceptually:
if (value == expected) { value = update; return true;} else { return false;}This operation happens atomically — no other thread can interleave between the comparison and the update.
Java’s CAS Support
Java exposes CAS via the sun.misc.Unsafe API and higher-level classes like AtomicInteger, AtomicReference, and AtomicStampedReference.
Example: Using CAS with AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
class CasCounter { private final AtomicInteger count = new AtomicInteger(0);
public void increment() { while (true) { int existing = count.get(); int newValue = existing + 1; if (count.compareAndSet(existing, newValue)) { return; } } }
public int get() { return count.get(); }}This loop repeatedly tries to update count until it succeeds — a spin loop. If another thread updates the value first, compareAndSet() fails and retries.
Benefits of Lock-Free Programming
- No blocking — threads don’t wait for locks.
- High scalability — ideal for low-contention counters, queues, and pools.
- Deadlock-free — CAS avoids mutual exclusion.
Pitfalls
- CPU spinning may waste cycles under high contention.
- ABA problem: a variable changes from A → B → A, fooling CAS into thinking it’s unchanged.
UseAtomicStampedReferenceorAtomicMarkableReferenceto track versions.
Example: Avoiding ABA
import java.util.concurrent.atomic.AtomicStampedReference;
class SafeReference<T> { private final AtomicStampedReference<T> ref = new AtomicStampedReference<>(null, 0);
public boolean update(T expected, T newValue) { int stamp = ref.getStamp(); return ref.compareAndSet(expected, newValue, stamp, stamp + 1); }}CAS operations form the foundation of lock-free data structures, such as ConcurrentLinkedQueue and ConcurrentHashMap.
🧩 2. Designing Thread-Safe Classes
Designing for concurrency means building components that can be safely used by multiple threads. Let’s explore several strategies and design patterns that ensure thread safety.
Strategy 1: Immutability
Immutable objects cannot be modified after construction — hence automatically thread-safe.
final class User { private final String name; private final int age;
public User(String name, int age) { this.name = name; this.age = age; }
public String getName() { return name; } public int getAge() { return age; }}- No synchronization needed.
- Safe to publish and share across threads.
- Use builder patterns for complex immutables.
Strategy 2: Thread Confinement
Restrict an object to be used only within one thread — e.g., local variables, thread-local storage.
ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));Each thread gets its own formatter — solving SimpleDateFormat’s thread-safety issue.
Strategy 3: Confinement via Stack and Message Passing
Avoid sharing at all. Instead of multiple threads updating shared state, communicate through queues.
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
new Thread(() -> { while (true) process(queue.take());}).start();This follows the actor model pattern — confinement through message passing.
Strategy 4: Using Thread-Safe Collections
Use Java’s java.util.concurrent structures instead of manual locking:
ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueue
They internally use fine-grained locks or lock-free algorithms for high concurrency.
Strategy 5: Synchronization at the Right Level
Use synchronization to protect invariants, not every field.
Bad example:
public synchronized void setX(int x) { this.x = x; }public synchronized void setY(int y) { this.y = y; }Good example:
public synchronized void move(int newX, int newY) { x = newX; y = newY;}Here, both fields are updated together, preserving invariants.
⚙️ 3. Concurrency Utilities in Depth
The java.util.concurrent package provides powerful utilities for synchronization, coordination, and task management. These utilities make high-level concurrency much easier and safer.
CountDownLatch
A CountDownLatch allows one or more threads to wait until a set of operations completes.
Example: Waiting for Multiple Services
import java.util.concurrent.CountDownLatch;
class AppStartup { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3);
Thread db = new Thread(() -> { init("Database"); latch.countDown(); }); Thread cache = new Thread(() -> { init("Cache"); latch.countDown(); }); Thread api = new Thread(() -> { init("API"); latch.countDown(); });
db.start(); cache.start(); api.start();
latch.await(); // Wait for all to complete System.out.println("All services ready!"); }
private static void init(String name) { System.out.println("Initializing " + name); try { Thread.sleep(1000); } catch (InterruptedException ignored) {} }}CyclicBarrier
A CyclicBarrier lets multiple threads wait at a common barrier point before continuing — often used in parallel computations.
import java.util.concurrent.CyclicBarrier;
class ParallelWorker { public static void main(String[] args) { CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads reached barrier, proceeding..."));
for (int i = 0; i < 3; i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " working..."); Thread.sleep(500); barrier.await(); } catch (Exception ignored) {} }).start(); } }}ExecutorService
Instead of manually creating threads, Java provides the Executor framework, which separates task submission from task execution.
Example: Fixed Thread Pool
import java.util.concurrent.*;
public class ThreadPoolDemo { public static void main(String[] args) throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) { int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " by " + Thread.currentThread().getName()); }); }
executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); }}Executors.newCachedThreadPool()— creates threads dynamically.newScheduledThreadPool()— for delayed or periodic tasks.newSingleThreadExecutor()— executes sequentially.
🔬 4. Testing and Detecting Race Conditions
Concurrency bugs are hard to reproduce because they depend on timing. Systematic testing requires tools, strategies, and assertions that expose timing-sensitive issues.
4.1. Stress Testing
Run concurrent operations repeatedly with random timing to expose issues.
ExecutorService pool = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) { pool.submit(() -> sharedResource.update());}pool.shutdown();pool.awaitTermination(1, TimeUnit.MINUTES);4.2. Thread Sleep Injection
Artificially insert sleeps or delays in suspicious code regions to force interleavings:
if (Math.random() < 0.1) Thread.sleep(1);This can expose races that otherwise occur rarely.
4.3. Use Concurrency Testing Tools
- Thread Sanitizer (TSan) — detects data races at runtime.
- Java Concurrency Stress (jcstress) — a JVM testing harness for concurrency correctness.
- FindBugs/SpotBugs concurrency detector — static analysis.
4.4. Code Reviews and Static Analysis
- Review shared state carefully.
- Check for non-atomic compound operations.
- Identify misuse of
volatile.
4.5. Logging and Monitoring
In production, capture concurrency metrics (e.g., queue sizes, thread states) using tools like VisualVM, JConsole, and Flight Recorder.
🏗️ 5. Real-World Examples and Design Patterns
Let’s see how concurrency appears in everyday system design.
Example 1: Thread-Safe Session Manager
import java.util.concurrent.ConcurrentHashMap;
class SessionManager { private final ConcurrentHashMap<String, String> sessions = new ConcurrentHashMap<>();
public void createSession(String userId) { sessions.put(userId, generateToken()); }
public String getSession(String userId) { return sessions.get(userId); }
public void removeSession(String userId) { sessions.remove(userId); }
private String generateToken() { return java.util.UUID.randomUUID().toString(); }}- Uses ConcurrentHashMap for thread-safe access.
- Avoids explicit synchronization.
Example 2: Lock-Free Counter
import java.util.concurrent.atomic.LongAdder;
class RequestCounter { private final LongAdder counter = new LongAdder();
public void increment() { counter.increment(); } public long getTotal() { return counter.sum(); }}LongAdder scales better than AtomicInteger under contention — it maintains multiple counters internally and aggregates them on demand.
Example 3: Producer-Consumer Queue
import java.util.concurrent.*;
class ProducerConsumer { public static void main(String[] args) { BlockingQueue<String> queue = new LinkedBlockingQueue<>();
Thread producer = new Thread(() -> { for (int i = 0; i < 5; i++) { try { queue.put("Task-" + i); System.out.println("Produced: Task-" + i); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } });
Thread consumer = new Thread(() -> { try { while (true) { String task = queue.take(); System.out.println("Consumed: " + task); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } });
producer.start(); consumer.start(); }}- Uses a blocking queue for synchronization.
- No need for explicit locks.
🎯 6. Java Concurrency Best Practices (2025 Edition)
| Category | Practice | Why |
|---|---|---|
| Design | Prefer immutability and thread confinement | Eliminates races by design |
| Locks | Use fine-grained locks or ReentrantLock only when necessary | Improves scalability |
| Atomic Types | Use AtomicInteger, LongAdder for counters | Lock-free and fast |
| Visibility | Use volatile only for simple flags | Avoids stale reads |
| Executors | Always use thread pools, not raw threads | Better lifecycle management |
| Shutdown | Always call shutdown() and awaitTermination() | Prevents thread leaks |
| Error Handling | Use Thread.UncaughtExceptionHandler | Capture background exceptions |
| Testing | Include concurrency stress tests in CI | Detects non-deterministic bugs |
💬 7. Interview Questions & How to Answer Them
Q1. What’s the difference between synchronized and ReentrantLock?
Answer:
- Both provide mutual exclusion.
ReentrantLockgives extra control: fairness,tryLock(), and interruptibility.- Use
synchronizedwhen simple,ReentrantLockwhen flexibility is needed.
Q2. Explain the difference between volatile and AtomicInteger.
Answer:
volatileguarantees visibility, not atomicity.AtomicIntegerguarantees both visibility and atomic updates via CAS.- Prefer
AtomicIntegerfor counters or accumulators.
Q3. What is the ABA problem in CAS?
Answer:
The ABA problem occurs when a variable changes from A → B → A, and a CAS operation mistakenly assumes it hasn’t changed.
Solution: Use AtomicStampedReference to include a version stamp.
Q4. Difference between CountDownLatch and CyclicBarrier?
Answer:
CountDownLatchis one-time use; used to wait for other threads to finish.CyclicBarrieris reusable; used to synchronize threads at a common point.
Q5. How do you test for race conditions?
Answer:
- Use stress tests with many threads.
- Insert artificial delays.
- Use tools like jcstress or thread sanitizer.
Q6. What’s the difference between ExecutorService and ForkJoinPool?
Answer:
ExecutorServicemanages independent tasks.ForkJoinPooloptimizes for tasks that can be split recursively (work-stealing).
Q7. Explain “happens-before” in simple terms.
Answer:
If one action happens-before another, the first’s results are guaranteed visible to the second.
Synchronization, volatile, and thread start/join establish happens-before edges.
🧠 Summary and Closing Thoughts
This part bridged foundational theory with real-world concurrency design:
- You learned lock-free programming and how CAS underlies atomic operations.
- You saw how to design thread-safe classes using immutability, confinement, and synchronization.
- You explored Java’s concurrency utilities that simplify synchronization and coordination.
- You learned how to test for race conditions and reason about happens-before.
- Finally, we looked at interview questions to prepare for advanced Java developer roles.
Concurrency is not about writing more threads — it’s about writing correct, scalable, and maintainable parallel systems.
“The art of concurrency isn’t about avoiding bugs. It’s about designing so they can’t exist.”