Java Multithreading and Concurrency Interview Questions

This section covers frequently asked interview questions related to Java multithreading and concurrency—essential concepts for building efficient and responsive applications.

What is Multithreading?

Multithreading is running multiple threads concurrently within a single program. This allows for parallel execution of tasks, leading to improved performance and responsiveness, especially useful in applications like game development. Key advantages include:

  • Threads share the same memory space.
  • Threads are lightweight (less resource-intensive than processes).
  • Efficient inter-thread communication.

More details on Multithreading

What is a Thread?

A thread is a lightweight subprocess; a separate path of execution within a process. A process can have multiple threads running concurrently. Threads within a process share resources but execute independently.

More details on Threads

Process vs. Thread

Feature Process Thread
Definition Running program Sub-process within a process
Independence Independent Part of a process
Memory Space Separate memory space Shared memory space
Context Switching Slower Faster
Communication Slower, more expensive Faster, cheaper

Inter-Thread Communication

Inter-thread communication allows threads to interact and exchange data. It's used to avoid busy-waiting (polling) and coordinate execution. Methods like wait(), notify(), and notifyAll() facilitate inter-thread communication.

`wait()` Method

The wait() method (defined in the Object class) pauses a thread's execution until another thread calls notify() or notifyAll() on the same object. This method is fundamental for synchronization and inter-thread communication.

Syntax

public final void wait() throws InterruptedException

`wait()` in Synchronized Blocks

The wait() method must be called from within a synchronized block to avoid IllegalMonitorStateException. It needs to be called in a synchronized context so that other threads can properly interact with the thread calling the `wait()` method.

Advantages of Multithreading

  • Increased responsiveness.
  • Improved performance (parallel execution).
  • Better resource utilization (shared memory).
  • Reduced server requirements.

Thread Lifecycle States

A thread's lifecycle includes:

  1. New: Thread object created but not yet started.
  2. Runnable: Ready to run (but might not be running yet).
  3. Running: Actively executing its task.
  4. Waiting/Blocked: Temporarily paused (waiting for a resource or event).
  5. Terminated: Finished execution.

Preemptive Scheduling vs. Time Slicing

Preemptive scheduling: The highest-priority runnable thread runs until it blocks or a higher-priority thread becomes available. Time slicing: Each thread runs for a short time slice, then returns to the ready queue for the scheduler to pick the next thread to run.

Context Switching

Context switching is saving and restoring a thread's state so that another thread can run. This allows multiple threads to share the CPU.

Creating Threads: `Thread` Class vs. `Runnable` Interface

Method Extending `Thread` Implementing `Runnable`
Inheritance Can't extend another class Can extend another class
Object Creation Each thread has its own object Multiple threads can share one object
Methods Many built-in thread methods Only the `run()` method

`join()` Method

The join() method makes the calling thread wait until the target thread completes its execution.

Syntax

public void join() throws InterruptedException

More details on the `join()` method

`sleep()` Method

The sleep() method pauses the current thread for a specified time. It doesn't release locks held by the thread.

Syntax

public static void sleep(long millis) throws InterruptedException

`wait()` vs. `sleep()`

Method `wait()` `sleep()`
Defined In Object class Thread class
Lock Release Releases the lock Doesn't release the lock

Starting a Thread Twice

Attempting to start a thread twice results in an IllegalThreadStateException.

Calling `run()` Directly

Calling run() directly executes the thread's code within the current thread; it doesn't create a new thread. Use start() to create and run a new thread.

Daemon Threads

Daemon threads provide background services. The JVM terminates when only daemon threads remain.

Changing a Thread to Daemon After Start

You cannot change a running thread to a daemon thread; it will throw an IllegalThreadStateException. The daemon flag must be set *before* the thread is started.

Shutdown Hooks

Shutdown hooks are threads automatically executed when the JVM shuts down (normal or abrupt). They are useful for performing cleanup tasks like closing resources or saving state.

Advanced Java Multithreading and Concurrency

This section delves into more complex multithreading and concurrency concepts frequently encountered in Java interviews.

Stopping a Shutdown Hook

A shutdown hook can be stopped by calling the Runtime.getRuntime().halt(int status) method. This forcefully terminates the JVM.

Interrupting a Thread

Interrupting a thread typically wakes a thread that's blocked in a sleep() or wait() call. The interrupt() method signals the interruption. The thread then handles the InterruptedException.

Synchronization in Java

Synchronization controls access to shared resources among multiple threads to prevent race conditions (inconsistent or unexpected results due to concurrent access). Java provides several ways to achieve synchronization:

  • Synchronized Methods: The entire method is synchronized.
  • Synchronized Blocks: Specific code blocks within a method are synchronized.
  • Static Synchronization: Synchronization on the class itself, not on a specific object instance.
Synchronized Block Syntax

synchronized (object) {
    // Code to be synchronized
}

More details on Synchronization

Purpose of Synchronized Blocks

Synchronized blocks provide fine-grained control over synchronization. Only one thread can execute a synchronized block on a particular object at a time.

More details on Synchronized Blocks

Locking Java Objects

Yes, using the synchronized keyword on a block of code locks the associated object. Only the thread that holds the lock can access the code within the synchronized block.

Static Synchronization

Static synchronized methods acquire a lock on the class itself, not on a specific object instance. This means that only one thread can execute a static synchronized method at any given time, regardless of how many objects of that class exist.

More details on Static Synchronization

`notify()` vs. `notifyAll()`

Both methods are used to wake up threads waiting on a synchronized object. notify() wakes up a single waiting thread (the choice is arbitrary). notifyAll() wakes up *all* threads waiting on that object.

Deadlocks

A deadlock is a situation where two or more threads are blocked indefinitely, each waiting for a resource held by another thread. This creates a standstill, preventing any progress.

More details on Deadlocks

Detecting and Avoiding Deadlocks

Detect deadlocks by analyzing thread dumps (using tools like jstack). To prevent deadlocks:

  • Avoid nested locks (acquiring locks in a different order can prevent deadlocks).
  • Minimize unnecessary locks.
  • Use join() to wait for thread completion instead of busy-waiting.

Thread Scheduler in Java

The Java Virtual Machine (JVM) includes a thread scheduler that manages the execution of threads. It decides which thread runs at any given time using scheduling algorithms (preemptive and time slicing).

Thread Stacks

Yes, each thread has its own private stack for storing local variables and managing method calls.

Thread Safety

A class or method is thread-safe if it can be accessed by multiple threads without causing race conditions (data corruption or inconsistencies). Thread safety is achieved through techniques like:

  • Synchronization
  • Volatile variables
  • Lock-based mechanisms
  • Atomic variables

Race Conditions

A race condition occurs when multiple threads access and modify a shared resource concurrently without proper synchronization, leading to unpredictable results.

The volatile Keyword

The volatile keyword ensures that changes to a variable are immediately visible to all threads. This enhances thread safety in specific situations but doesn't fully replace the need for synchronization in complex scenarios.

Thread Pools

A thread pool is a collection of worker threads that are reused to execute tasks, enhancing performance and resource utilization.

Concurrency API Components

The Java Concurrency API (java.util.concurrent package) provides classes and interfaces for concurrent programming, including:

  • Executor
  • ExecutorService
  • ScheduledExecutorService
  • Future
  • BlockingQueue
  • Various locking mechanisms

Executor Interface

The Executor interface provides a simple way to execute tasks. The execute() method submits a Runnable task for execution.

Example

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new MyRunnable());

BlockingQueue

A BlockingQueue is a queue that blocks operations (put or take) until space is available or an element exists. It's useful for producer-consumer scenarios.

Example: Producer-Consumer with BlockingQueue

BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
// ... Producer thread puts elements into queue ...
// ... Consumer thread takes elements from queue ...

Stopping Shutdown Hooks

You can forcefully stop a shutdown hook (and terminate the JVM) using Runtime.getRuntime().halt(int status). However, this is generally less preferred than allowing the shutdown hook to complete its tasks gracefully.

More details on stopping Shutdown Hooks

When to Interrupt a Thread

Interrupting a thread is typically used to signal that it should stop what it's doing (e.g., exit a loop). The interrupt() method sets an interrupt flag; it's up to the thread to check this flag and respond accordingly (often by handling an InterruptedException).

More details on interrupting threads

Synchronization

Synchronization in Java prevents race conditions (multiple threads accessing shared resources concurrently). Key approaches include:

  • Synchronized methods (the entire method is synchronized).
  • Synchronized blocks (only a portion of the code is synchronized).
  • Static synchronized methods (synchronization on the class level).

Synchronization ensures that only one thread can access a shared resource at a time, preventing data corruption and inconsistencies.

More details on Synchronization

Synchronized Blocks

Synchronized blocks provide fine-grained control over synchronization, allowing you to lock specific parts of your code rather than entire methods. This is generally more efficient than synchronizing entire methods.

More details on Synchronized Blocks

Locking Objects

Yes, you can lock an object using a synchronized block. Only the thread holding the lock can access the synchronized code block, preventing concurrent access.

Static Synchronization

A static synchronized method acquires a lock on the class itself, not on individual objects. This is useful for synchronizing access to static variables or methods.

More details on Static Synchronization

`notify()` vs. `notifyAll()`

Both methods are used within synchronized blocks to wake up waiting threads. notify() arbitrarily chooses one thread to wake up. notifyAll() wakes up all waiting threads.

Deadlocks

A deadlock occurs when two or more threads are blocked indefinitely, each waiting for the other to release a resource. This creates a standstill in the program's execution. Careful resource management is crucial to avoid deadlocks.

More details on Deadlocks

Detecting and Avoiding Deadlocks

You can detect deadlocks by examining thread dumps. To avoid them:

  • Avoid nested locks (acquire locks in a consistent order).
  • Avoid unnecessary locking.
  • Use the join() method to wait for other threads to finish.

Thread Schedulers

The Java Virtual Machine (JVM) manages thread scheduling, deciding which thread gets CPU time. Common scheduling algorithms include preemptive scheduling and time slicing.

Thread Stacks

Each thread in a Java program has its own separate stack. This is essential for thread independence.

Achieving Thread Safety

Thread safety means that multiple threads can access shared data without causing problems (race conditions). Techniques for achieving thread safety include:

  • Synchronization (using synchronized)
  • Volatile variables (changes are immediately visible to all threads)
  • Lock-based mechanisms (explicit locks)
  • Atomic variables (operations are atomic, indivisible)

Race Conditions

A race condition arises when multiple threads access and modify shared data concurrently without proper synchronization. This leads to unpredictable results because the final state depends on the unpredictable order of thread execution.

The volatile Keyword

The volatile keyword is a weaker form of synchronization. It ensures that a variable's value is visible across threads but doesn't provide mutual exclusion (only one thread can access a shared resource at a time).

Thread Pools

Thread pools reuse worker threads to handle tasks, improving performance by reducing the overhead of creating and destroying threads for each task.

Concurrency API Components

The Java Concurrency Utilities (java.util.concurrent) provide tools for concurrent programming:

  • Executor
  • ExecutorService
  • ScheduledExecutorService
  • Future
  • BlockingQueue
  • Lock implementations
  • Synchronization aids

Callable vs. Runnable

Interface Callable Runnable
Return Value Can return a value Cannot return a value
Exceptions Can throw checked exceptions Can only throw unchecked exceptions

Atomic Actions

Atomic actions are operations that are guaranteed to execute as a single, indivisible unit. This is essential for thread safety when multiple threads access shared variables.

`Lock` Interface

The Lock interface provides more flexible synchronization than the synchronized keyword. It offers features like:

  • More control over locking order.
  • Support for timeouts (try to acquire a lock for a specified time).
  • Interruptible locks (a thread can be interrupted while waiting for a lock).

`ExecutorService` Interface

ExecutorService extends Executor, adding lifecycle management capabilities (starting, shutting down, etc.).

Example: ExecutorService

ExecutorService executor = Executors.newFixedThreadPool(5); // Creates a pool of 5 threads
// ... submit tasks using executor.submit() ...
executor.shutdown(); // Gracefully shut down the executor

Synchronous vs. Asynchronous Programming

Synchronous: A thread completes one task before starting another. Asynchronous: Multiple threads can work on different tasks concurrently.

`Callable` and `Future`

Callable is similar to Runnable but returns a result and can throw checked exceptions. Future represents the result of an asynchronous computation (obtained using get()).

`ScheduledExecutorService` vs. `ExecutorService`

ScheduledExecutorService extends ExecutorService, adding the ability to schedule tasks to run after a delay or at fixed intervals.

`FutureTask` Class

FutureTask provides a concrete implementation of Future. It wraps a Callable or Runnable task and provides methods to check the task's status, retrieve results, and cancel the task.