Process vs Thread

Understanding the fundamental difference between a process and a thread is crucial. A process operates with its own isolated memory space and operating system resources. In contrast, threads exist within a process, sharing its heap memory and file handles. However, each thread maintains its own independent stack and program counter. This shared memory is the double-edged sword of multithreading: it makes threads inexpensive to create and manage, but it introduces significant dangers in coordination, as any thread can potentially read from or corrupt the data visible to all other threads.

Platform Thread vs Virtual Thread

Java has evolved its threading model. A platform thread is a direct, one-to-one mapping to an operating system thread. These are relatively expensive due to their large, megabyte-sized stacks and reliance on kernel-level scheduling. Consequently, thread pools are typically capped at a few hundred instances. Virtual threads, introduced in Java 21 as part of Project Loom, offer a more scalable alternative. These are lightweight threads managed by the Java Virtual Machine (JVM). They are backed by a small pool of underlying "carrier" platform threads. When a virtual thread performs a blocking I/O operation, it can unmount itself from its carrier thread, allowing that carrier to execute another virtual thread. This design enables the JVM to manage millions of virtual threads efficiently.

Diagram illustrating the relationship between platform threads, virtual threads, and carrier threads in Java's Loom project.

Synchronization Primitives

Effective multithreading relies on robust synchronization mechanisms to prevent race conditions and ensure data integrity. Key primitives include:

Synchronized Keyword

The synchronized keyword is Java's most basic tool for thread safety. It can be applied to methods or blocks of code. When a thread enters a synchronized block or method, it acquires an intrinsic lock associated with an object. Other threads attempting to enter the same synchronized block on the same object will be blocked until the first thread exits the block and releases the lock. This ensures that only one thread can execute the critical section at a time.

Volatile Keyword

The volatile keyword guarantees that writes to a variable are immediately visible to other threads and that reads from the variable will see the latest written value. It prevents compiler and processor reordering of reads and writes to volatile variables, providing a weaker form of synchronization than synchronized. It's primarily used for visibility guarantees, not for atomic operations on non-atomic types.

Atomic Variables

The java.util.concurrent.atomic package provides classes like AtomicInteger, AtomicLong, and AtomicReference. These classes offer lock-free, thread-safe operations using hardware-level Compare-And-Swap (CAS) instructions. For example, incrementAndGet() on an AtomicInteger performs an atomic increment without requiring explicit locks, which can offer better performance under high contention.

ReentrantLock

ReentrantLock, part of java.util.concurrent.locks, provides more flexibility than the synchronized keyword. It allows for timed lock attempts (tryLock()), interruptible lock acquisition, and fairness policies. It also enables the use of conditions (Condition objects) for more complex wait/notify scenarios, offering finer-grained control over thread synchronization.

Concurrency Utilities

Java's java.util.concurrent package is a rich source of advanced concurrency tools:

Executors Framework

The ExecutorService interface and its implementations (e.g., ThreadPoolExecutor) provide a robust way to manage thread pools. Instead of manually creating and managing threads, you submit tasks (Runnable or Callable) to the ExecutorService, which handles thread lifecycle, reuse, and task queuing. This decouples task submission from task execution, simplifying concurrency management.

Concurrent Collections

This package includes thread-safe implementations of common collection interfaces, such as ConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueue implementations like ArrayBlockingQueue and LinkedBlockingQueue. These collections are designed for high-concurrency scenarios, often employing techniques like lock striping or non-blocking algorithms to achieve better performance than synchronized wrappers (e.g., Collections.synchronizedMap).

CountDownLatch, CyclicBarrier, Semaphore

These are synchronization aids for more complex coordination patterns:

  • CountDownLatch: Allows one or more threads to wait until a set of operations being performed in other threads completes. It's initialized with a count, and threads decrement the count as they complete their work. Threads waiting on the latch are released when the count reaches zero.
  • CyclicBarrier: A barrier that allows a set of threads to all wait for each other to reach a common barrier point. It's like a rendezvous point. Once all threads arrive, they are released, and the barrier can be reused.
  • Semaphore: Controls access to a limited number of resources. It maintains a set of permits. Threads acquire a permit to access a resource, and release it when done. If no permits are available, threads block until one is released.

Common Concurrency Issues and Solutions

Interviewers often probe understanding of potential pitfalls in concurrent programming:

Race Conditions

A race condition occurs when the outcome of a computation depends on the non-deterministic timing or interleaving of operations by multiple threads accessing shared mutable data. For example, a simple counter increment count++ is not atomic; it involves reading the current value, incrementing it, and writing it back. If two threads read the same value before either writes back, one increment will be lost.

Deadlocks

A deadlock occurs when two or more threads are blocked forever, each waiting for a resource held by another thread in the cycle. The classic example is Thread A holding Lock X and waiting for Lock Y, while Thread B holds Lock Y and waits for Lock X. Preventing deadlocks involves strategies like acquiring locks in a consistent order, using timed lock attempts, or employing deadlock detection mechanisms.

Livelocks

A livelock is similar to a deadlock in that no thread makes progress, but threads are not blocked. Instead, they are actively trying to do something, but their actions repeatedly prevent them from completing. For instance, two threads might repeatedly yield to each other, never acquiring the necessary resources.

Starvation

Starvation occurs when a thread is perpetually denied access to a resource or CPU time, even though it is ready to execute. This can happen if higher-priority threads or threads that are more aggressive in acquiring locks consistently monopolize resources.

Visibility Problems

These arise when changes made by one thread to a shared variable are not immediately visible to other threads. This can lead to threads operating on stale data. The volatile keyword and proper use of synchronized blocks address visibility issues.

Advanced Topics

Beyond the basics, interviews may touch upon:

Thread Safety vs. Immutability

Immutable objects are inherently thread-safe because their state cannot change after creation. This simplifies concurrent programming significantly, as multiple threads can safely share references to immutable objects without any need for synchronization.

Thread Pools and Lifecycle Management

Proper management of thread pools is vital for performance and resource utilization. This includes choosing appropriate pool sizes (fixed, cached, scheduled), handling rejected tasks, and gracefully shutting down the pool to ensure all submitted tasks are completed.

Fork/Join Framework

This framework is designed for recursive, divide-and-conquer algorithms. It uses a work-stealing algorithm where worker threads can steal tasks from the queues of other idle threads, leading to efficient CPU utilization for parallelizable computations.

CompletableFuture

CompletableFuture provides a powerful way to write asynchronous, non-blocking code in Java. It allows chaining of operations, handling results, exceptions, and combining multiple asynchronous computations in a fluent API, avoiding the complexity of manual thread management and callbacks.

Java Memory Model (JMM)

Understanding the JMM is essential for grasping how threads interact with memory. It defines the semantics of read and write operations on variables and how these operations are ordered by the JVM and the hardware. Concepts like happens-before relationships, memory barriers, and cache coherency are key here.

Performance Tuning

Optimizing concurrent applications involves profiling to identify bottlenecks, choosing the right synchronization primitives, tuning thread pool sizes, and understanding the impact of garbage collection on concurrent performance.