Skip to content

Advanced Topics

This guide covers advanced topics in Java concurrency, including the Java Memory Model, visibility issues, and best practices for concurrent programming.


The Java Memory Model (JMM) defines how threads interact through memory and establishes rules for visibility, atomicity, and ordering of operations.

  • Main Memory: Shared memory accessible by all threads
  • Working Memory: Each thread has its own cache/working memory
  • Visibility: Changes made by one thread may not be immediately visible to others
  • Happens-Before Relationship: Guarantees that memory operations in one thread are visible to another
  1. Program Order Rule: Each action in a thread happens-before every subsequent action in the same thread
  2. Monitor Lock Rule: An unlock on a monitor happens-before every subsequent lock on that same monitor
  3. Volatile Field Rule: A write to a volatile field happens-before every subsequent read of that field
  4. Thread Start Rule: A call to Thread.start() happens-before any actions in the started thread
  5. Thread Termination Rule: Any action in a thread happens-before another thread detects that thread has terminated (via Thread.join() or Thread.isAlive())
  6. Transitivity: If A happens-before B, and B happens-before C, then A happens-before C
public class VisibilityProblem {
private boolean flag = false;
private int value = 0;
public void writer() {
value = 42;
flag = true; // Without proper synchronization, this may not be visible
}
public void reader() {
if (flag) { // May read stale value
System.out.println(value); // May not see 42
}
}
}
  1. Synchronized Blocks/Methods:
public synchronized void writer() {
value = 42;
flag = true;
}
public synchronized void reader() {
if (flag) {
System.out.println(value); // Always sees 42
}
}
  1. Volatile Variables:
private volatile boolean flag = false;
private int value = 0;
public void writer() {
value = 42;
flag = true; // Write to volatile creates happens-before relationship
}
public void reader() {
if (flag) { // Read of volatile creates happens-before relationship
System.out.println(value); // Always sees 42
}
}
  1. Atomic Variables:
private AtomicBoolean flag = new AtomicBoolean(false);
private AtomicInteger value = new AtomicInteger(0);
public void writer() {
value.set(42);
flag.set(true);
}
public void reader() {
if (flag.get()) {
System.out.println(value.get());
}
}

The JVM and CPU may reorder operations for optimization as long as the program’s behavior appears the same from the perspective of a single thread.

// Original code
x = 1;
y = 2;
// Possible reordering
y = 2;
x = 1;

This reordering is problematic in concurrent contexts where other threads may observe the operations in a different order than intended.

Memory barriers (or fences) prevent certain types of reordering:

  • Read barriers: Ensure all reads before the barrier are completed before any read after the barrier
  • Write barriers: Ensure all writes before the barrier are completed before any write after the barrier
  • Full barriers: Combine read and write barriers

In Java, these barriers are implicitly created by:

  • Synchronized blocks/methods
  • Volatile reads/writes
  • Atomic variable operations
  • Concurrent collection operations

Java prohibits “out-of-thin-air” values, which are values that could appear in a program without being written by any thread.


  • Use immutable objects when possible
  • Encapsulate mutable state within classes
  • Use thread confinement when appropriate
  • Pass copies rather than references
  • Prefer concurrent collections over synchronized collections
  • Use Executors instead of raw threads
  • Use atomic variables instead of synchronized for simple counters
  • Use synchronizers (CountDownLatch, CyclicBarrier, etc.) for coordination

3. Follow Proper Synchronization Practices

Section titled “3. Follow Proper Synchronization Practices”
  • Synchronize all access to shared mutable data
  • Keep synchronized blocks as small as possible
  • Avoid holding locks during lengthy operations
  • Acquire locks in a consistent order to prevent deadlocks
  • Release resources in finally blocks
  • Document thread safety guarantees
  • Make classes either clearly thread-safe or clearly not thread-safe
  • Consider thread safety during code reviews
  • Use final fields wherever possible
  • Check for race conditions
  • Be careful with double-checked locking
  • Don’t ignore InterruptedException
  • Avoid excessive synchronization
  • Don’t rely on thread scheduling

  • Use fine-grained locking
  • Minimize lock duration
  • Use lock striping (e.g., in ConcurrentHashMap)
  • Use non-blocking algorithms when appropriate
  • Match pool size to workload characteristics
  • For CPU-bound tasks: threads ≈ number of CPU cores
  • For I/O-bound tasks: threads > number of CPU cores
  • Monitor and adjust pool sizes
  • Use non-blocking I/O
  • Move blocking operations off the critical path
  • Consider asynchronous alternatives
  • Use tools like JMH for microbenchmarking
  • Profile with tools like VisualVM, YourKit, or JProfiler
  • Monitor thread contention with JMX

1. What is the difference between process and thread?

Section titled “1. What is the difference between process and thread?”

Answer: A process is an independent program with its own memory space, while a thread is a lightweight execution unit within a process that shares memory with other threads in the same process.

Key differences:

  • Processes have separate memory spaces; threads share memory
  • Process creation is more resource-intensive than thread creation
  • Inter-process communication is more complex than inter-thread communication
  • A crash in one process doesn’t affect others; a thread crash may affect the entire process

2. How does the Java Memory Model ensure visibility between threads?

Section titled “2. How does the Java Memory Model ensure visibility between threads?”

Answer: The Java Memory Model ensures visibility through happens-before relationships established by:

  1. Synchronization actions (locks, unlocks)
  2. Volatile variable accesses
  3. Thread lifecycle events (start, join)
  4. Transitivity of happens-before relationships

Without these mechanisms, one thread’s changes might not be visible to other threads due to caching, compiler optimizations, or CPU reordering.

3. What is the significance of the volatile keyword in Java?

Section titled “3. What is the significance of the volatile keyword in Java?”

Answer: The volatile keyword ensures:

  1. Visibility: Changes to a volatile variable are immediately visible to all threads
  2. Ordering: Prevents reordering of operations around volatile accesses
  3. Atomicity: For single reads/writes of the variable (but not for compound operations like increment)

It’s useful for flags and state variables that are read by multiple threads but doesn’t provide mutual exclusion or atomicity for compound operations.

4. How would you implement a thread-safe singleton in Java?

Section titled “4. How would you implement a thread-safe singleton in Java?”

Answer: Several approaches:

1. Eager initialization:

public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}

2. Synchronized method:

public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

3. Double-checked locking (Java 5+):

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;
}
}

4. Holder class idiom (preferred):

public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

5. Enum singleton (simplest and most effective):

public enum Singleton {
INSTANCE;
// Methods and fields
public void doSomething() {
// Implementation
}
}

5. What are some common concurrency issues and how do you avoid them?

Section titled “5. What are some common concurrency issues and how do you avoid them?”

Answer:

1. Race Conditions:

  • Issue: Multiple threads access shared data concurrently, and the outcome depends on the relative timing of their execution
  • Solution: Use proper synchronization (locks, atomic variables, etc.)

2. Deadlocks:

  • Issue: Two or more threads are blocked forever, each waiting for the other to release a lock
  • Solution: Acquire locks in a consistent order, use timeouts, avoid nested locks

3. Livelocks:

  • Issue: Threads keep changing their state in response to each other without making progress
  • Solution: Add randomness to retry logic, implement backoff strategies

4. Starvation:

  • Issue: A thread is perpetually denied access to resources it needs
  • Solution: Use fair locks, limit thread priorities, implement timeouts

5. Memory Visibility Issues:

  • Issue: Changes made by one thread are not visible to other threads
  • Solution: Use proper synchronization, volatile variables, or atomic classes

6. Thread Leaks:

  • Issue: Threads are created but never terminated
  • Solution: Use thread pools, properly shut down executors, use daemon threads when appropriate

6. How do you ensure thread safety in Java?

Section titled “6. How do you ensure thread safety in Java?”

Answer: Several strategies:

  1. Immutability: Make objects immutable so they can be freely shared

    public final class ImmutablePoint {
    private final int x;
    private final int y;
    public ImmutablePoint(int x, int y) {
    this.x = x;
    this.y = y;
    }
    public int getX() { return x; }
    public int getY() { return y; }
    }
  2. Synchronization: Use synchronized methods/blocks

    public synchronized void increment() {
    count++;
    }
  3. Atomic variables: Use classes from java.util.concurrent.atomic

    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
    count.incrementAndGet();
    }
  4. Thread confinement: Restrict data to a single thread

    ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(
    () -> new SimpleDateFormat("yyyy-MM-dd"));
  5. Concurrent collections: Use thread-safe collections

    Map<String, String> map = new ConcurrentHashMap<>();
  6. Locks: Use explicit locks for more control

    private final Lock lock = new ReentrantLock();
    public void increment() {
    lock.lock();
    try {
    count++;
    } finally {
    lock.unlock();
    }
    }
  7. Volatile: For visibility of simple flags

    private volatile boolean running = true;

7. What is the difference between ConcurrentHashMap and Hashtable?

Section titled “7. What is the difference between ConcurrentHashMap and Hashtable?”

Answer:

FeatureConcurrentHashMapHashtable
Thread safetyYes (segment/stripe locking)Yes (single lock)
PerformanceHigh concurrencyLower concurrency
Null valuesDoesn’t allow null keys or valuesDoesn’t allow null keys or values
IteratorWeakly consistentFail-fast
Atomic operationsputIfAbsent, replace, etc.None
Locking granularityFine-grained (Java 7) or node-level (Java 8+)Coarse-grained

ConcurrentHashMap is generally preferred for concurrent applications due to its better performance under high concurrency.

8. How do you handle InterruptedException?

Section titled “8. How do you handle InterruptedException?”

Answer: There are three common approaches:

  1. Propagate the exception:

    public void myMethod() throws InterruptedException {
    Thread.sleep(1000);
    }
  2. Restore the interrupt status:

    public void myMethod() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // Restore flag
    // Handle the interruption or log it
    }
    }
  3. Wrap in an unchecked exception:

    public void myMethod() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new RuntimeException("Interrupted", e);
    }
    }

Never silently swallow InterruptedException without restoring the interrupt status.