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.
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:
- New: Thread object created but not yet started.
- Runnable: Ready to run (but might not be running yet).
- Running: Actively executing its task.
- Waiting/Blocked: Temporarily paused (waiting for a resource or event).
- 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.
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.
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.