Advanced Topics
This guide covers advanced topics in Java concurrency, including the Java Memory Model, visibility issues, and best practices for concurrent programming.
Java Memory Model
Section titled “Java Memory Model”The Java Memory Model (JMM) defines how threads interact through memory and establishes rules for visibility, atomicity, and ordering of operations.
Key Concepts
Section titled “Key Concepts”- 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
Happens-Before Rules
Section titled “Happens-Before Rules”- Program Order Rule: Each action in a thread happens-before every subsequent action in the same thread
- Monitor Lock Rule: An unlock on a monitor happens-before every subsequent lock on that same monitor
- Volatile Field Rule: A write to a volatile field happens-before every subsequent read of that field
- Thread Start Rule: A call to
Thread.start()happens-before any actions in the started thread - Thread Termination Rule: Any action in a thread happens-before another thread detects that thread has terminated (via
Thread.join()orThread.isAlive()) - Transitivity: If A happens-before B, and B happens-before C, then A happens-before C
Example: Visibility Problem
Section titled “Example: Visibility Problem”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 } }}Solutions to Visibility Problems
Section titled “Solutions to Visibility Problems”- Synchronized Blocks/Methods:
public synchronized void writer() { value = 42; flag = true;}
public synchronized void reader() { if (flag) { System.out.println(value); // Always sees 42 }}- 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 }}- 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()); }}Memory Visibility
Section titled “Memory Visibility”Reordering Effects
Section titled “Reordering Effects”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 codex = 1;y = 2;
// Possible reorderingy = 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
Section titled “Memory Barriers”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
Out-of-Thin-Air Safety
Section titled “Out-of-Thin-Air Safety”Java prohibits “out-of-thin-air” values, which are values that could appear in a program without being written by any thread.
Concurrency Best Practices
Section titled “Concurrency Best Practices”1. Minimize Shared Mutable State
Section titled “1. Minimize Shared Mutable State”- Use immutable objects when possible
- Encapsulate mutable state within classes
- Use thread confinement when appropriate
- Pass copies rather than references
2. Use Higher-Level Concurrency Utilities
Section titled “2. Use Higher-Level Concurrency Utilities”- 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
4. Design for Thread Safety
Section titled “4. Design for Thread Safety”- 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
5. Avoid Common Pitfalls
Section titled “5. Avoid Common Pitfalls”- Check for race conditions
- Be careful with double-checked locking
- Don’t ignore InterruptedException
- Avoid excessive synchronization
- Don’t rely on thread scheduling
Performance Considerations
Section titled “Performance Considerations”1. Reduce Contention
Section titled “1. Reduce Contention”- Use fine-grained locking
- Minimize lock duration
- Use lock striping (e.g., in ConcurrentHashMap)
- Use non-blocking algorithms when appropriate
2. Thread Pool Tuning
Section titled “2. Thread Pool Tuning”- 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
3. Avoid Blocking Operations
Section titled “3. Avoid Blocking Operations”- Use non-blocking I/O
- Move blocking operations off the critical path
- Consider asynchronous alternatives
4. Measure and Profile
Section titled “4. Measure and Profile”- Use tools like JMH for microbenchmarking
- Profile with tools like VisualVM, YourKit, or JProfiler
- Monitor thread contention with JMX
Common Interview Questions
Section titled “Common Interview Questions”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:
- Synchronization actions (locks, unlocks)
- Volatile variable accesses
- Thread lifecycle events (start, join)
- 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:
- Visibility: Changes to a volatile variable are immediately visible to all threads
- Ordering: Prevents reordering of operations around volatile accesses
- 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:
-
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; }} -
Synchronization: Use synchronized methods/blocks
public synchronized void increment() {count++;} -
Atomic variables: Use classes from java.util.concurrent.atomic
private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();} -
Thread confinement: Restrict data to a single thread
ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); -
Concurrent collections: Use thread-safe collections
Map<String, String> map = new ConcurrentHashMap<>(); -
Locks: Use explicit locks for more control
private final Lock lock = new ReentrantLock();public void increment() {lock.lock();try {count++;} finally {lock.unlock();}} -
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:
| Feature | ConcurrentHashMap | Hashtable |
|---|---|---|
| Thread safety | Yes (segment/stripe locking) | Yes (single lock) |
| Performance | High concurrency | Lower concurrency |
| Null values | Doesn’t allow null keys or values | Doesn’t allow null keys or values |
| Iterator | Weakly consistent | Fail-fast |
| Atomic operations | putIfAbsent, replace, etc. | None |
| Locking granularity | Fine-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:
-
Propagate the exception:
public void myMethod() throws InterruptedException {Thread.sleep(1000);} -
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}} -
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.