Basics of Multithreading
Basics of Multithreading Interview with follow-up questions
1. What is multithreading in Java?
Multithreading is running multiple threads — independent paths of execution — concurrently within a single process, sharing the same heap/memory. It lets a program do several things at once: keep a UI responsive while working in the background, handle many requests in parallel, or split a CPU task across cores.
Runnable task = () -> System.out.println("on " + Thread.currentThread());
new Thread(task).start(); // runs concurrently with the main thread
Concepts interviewers expect: the main thread starts every app; threads have a lifecycle (new → runnable → running → blocked/waiting → terminated); and because threads share memory, you need synchronization to avoid race conditions. Distinguish concurrency (interleaving tasks) from parallelism (truly simultaneous on multiple cores).
The crucial 2026 update: don't create raw Threads in modern code — use the ExecutorService / thread pools (java.util.concurrent), CompletableFuture for async composition, and especially virtual threads (Java 21, Project Loom). Virtual threads are lightweight JVM-managed threads — you can have millions of them — making the simple "thread-per-task" blocking style scale massively for I/O-bound work without the cost of OS threads:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(task);
}
Knowing virtual threads (vs OS/platform threads) is exactly what distinguishes a current answer.
Follow-up 1
Can you explain the advantages of multithreading?
There are several advantages of multithreading in Java:
Increased responsiveness: Multithreading allows a program to remain responsive even when performing time-consuming tasks. By executing tasks concurrently, the program can continue to respond to user input or perform other operations.
Improved performance: Multithreading can improve the overall performance of a program by utilizing the available resources more efficiently. It allows for parallel execution of tasks, which can lead to faster completion times.
Simplified design: Multithreading can simplify the design of certain types of applications. For example, in a graphical user interface (GUI) application, using separate threads for user interface updates and background tasks can make the code more modular and easier to maintain.
Resource sharing: Multithreading allows multiple threads to share the same resources, such as memory or files. This can be useful for tasks that require coordination or communication between different parts of the program.
Follow-up 2
What are the ways to create a thread in Java?
There are two main ways to create a thread in Java:
- Extending the Thread class: By extending the Thread class, you can create a new class that represents a thread. This class must override the
run()method, which contains the code that will be executed when the thread is started. To create and start a thread, you can instantiate an object of the new class and call itsstart()method.
Example:
public class MyThread extends Thread {
public void run() {
// Code to be executed by the thread
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}```
2. Implementing the Runnable interface: The Runnable interface defines a single method, `run()`, that represents the code to be executed by the thread. By implementing this interface, you can create a new class that represents a thread. To create and start a thread, you can instantiate an object of the new class, create a new Thread object with the Runnable object as a parameter, and call the `start()` method of the Thread object.
Example:
```java
public class MyRunnable implements Runnable {
public void run() {
// Code to be executed by the thread
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}```
Follow-up 3
What is the difference between a process and a thread?
In Java, a process is an instance of a program that is being executed. It has its own memory space and resources, and can run independently of other processes. On the other hand, a thread is a unit of execution within a process. It represents an independent path of execution and shares the same memory space and resources as other threads within the same process.
Here are some key differences between a process and a thread:
Memory and resources: Each process has its own memory space and resources, while threads within a process share the same memory space and resources.
Creation and termination: Processes are created and terminated independently of each other, while threads are created and terminated within a process.
Communication and synchronization: Inter-process communication and synchronization require explicit mechanisms, such as inter-process communication (IPC) or shared memory. Threads within a process can communicate and synchronize with each other more easily, as they share the same memory space.
Overhead: Creating and managing processes typically has more overhead compared to creating and managing threads. Processes require separate memory space and resources, while threads within a process can be created and managed more efficiently.
Fault isolation: Processes provide better fault isolation compared to threads. If one process crashes, it does not affect other processes. However, if a thread crashes, it can potentially crash the entire process.
2. How can we create a thread in Java?
The two classic ways:
// 1. Implement Runnable (preferred — favors composition, leaves you free to extend another class)
Runnable task = () -> doWork(); // lambda, since Runnable is functional
new Thread(task).start();
// 2. Extend Thread (rarely recommended — uses up your single inheritance)
class Worker extends Thread { public void run() { doWork(); } }
new Worker().start();
Key point: call start() (which spawns a new thread and invokes run() on it), not run() directly — calling run() just executes it on the current thread, no concurrency. Prefer Runnable (or Callable when you need a return value/checked exception) over extending Thread.
The crucial 2026 update interviewers want: in modern code you rarely create threads manually at all. Use the java.util.concurrent facilities:
ExecutorService pool = Executors.newFixedThreadPool(8);
Future f = pool.submit(() -> compute()); // Callable
// or virtual threads (Java 21) for massive I/O concurrency:
ExecutorService vexec = Executors.newVirtualThreadPerTaskExecutor();
Thread vt = Thread.ofVirtual().start(task);
So the complete answer is: "Runnable or extend Thread are the language basics, but use an ExecutorService/thread pool, CompletableFuture for async, and virtual threads for high-throughput blocking workloads — manual new Thread() is discouraged in production."
Follow-up 1
What is the difference between implementing Runnable and extending Thread?
When creating a thread, it is generally recommended to implement the Runnable interface rather than extending the Thread class. This is because Java does not support multiple inheritance, so if you extend the Thread class, you cannot extend any other class.
By implementing the Runnable interface, you can still extend other classes and implement multiple interfaces. Additionally, implementing Runnable separates the thread's behavior (the run() method) from the thread's identity (the Thread object), which can lead to better design and code organization.
Follow-up 2
Can you explain the lifecycle of a thread in Java?
The lifecycle of a thread in Java consists of several states:
New: When a thread is created but not yet started.
Runnable: When a thread is ready to run, it enters the runnable state. It may or may not be currently executing, as it depends on the thread scheduler.
Running: When a thread is executing, it is in the running state.
Blocked: When a thread is waiting for a monitor lock to enter a synchronized block or method, it is in the blocked state.
Waiting: When a thread is waiting indefinitely for another thread to perform a particular action, it is in the waiting state.
Timed Waiting: When a thread is waiting for a specified amount of time, it is in the timed waiting state.
Terminated: When a thread has completed its execution or terminated abnormally, it is in the terminated state.
Follow-up 3
What are the states of a thread in Java?
The states of a thread in Java are:
- New
- Runnable
- Running
- Blocked
- Waiting
- Timed Waiting
- Terminated
3. What is the purpose of the join() method in Java multithreading?
join() makes the calling thread wait until the target thread finishes (terminates). It's how you coordinate completion — e.g. the main thread starts several workers, then join()s each so it doesn't proceed until they're all done.
Thread t = new Thread(task);
t.start();
t.join(); // current thread blocks here until t completes
// ... safe to use t's results now
Details interviewers want: join() throws InterruptedException (so it must be handled — typically restore the interrupt flag); overloads join(millis) wait at most a timeout; and join() establishes a happens-before relationship, so the joining thread reliably sees the results the joined thread produced.
The 2026 framing: manual start()/join() is fine to know, but modern code coordinates completion with higher-level tools — ExecutorService.invokeAll/Future.get, CompletableFuture.allOf(...), or structured concurrency (StructuredTaskScope, finalized around Java 25) which cleanly forks subtasks and joins them as a unit:
List> results = executor.invokeAll(tasks); // waits for all
So join() answers "wait for this thread to finish," but in real apps you'd usually express that through an executor/CompletableFuture/structured concurrency rather than hand-managing threads.
Follow-up 1
Can you provide an example of using the join() method?
Sure! Here's an example:
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 started");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 finished");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 started");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 finished");
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("All threads finished");
}
}
In this example, we create two threads (thread1 and thread2) and start them. We then call the join() method on both threads to wait for them to finish before printing "All threads finished".
Follow-up 2
What happens if we call the join() method on a thread that is already running?
If we call the join() method on a thread that is already running, the calling thread will wait until the target thread finishes its execution. If the target thread has already finished, the join() method returns immediately.
Follow-up 3
What is the difference between the join() and sleep() methods in Java?
The join() and sleep() methods in Java are both used for thread synchronization, but they have some differences:
The join() method is used to wait for a thread to complete its execution, while the sleep() method is used to pause the execution of a thread for a specified amount of time.
The join() method is called on a thread object, while the sleep() method is called on the Thread class itself.
The join() method throws InterruptedException, while the sleep() method also throws InterruptedException but it is optional to handle it.
The join() method can be used to wait for multiple threads to finish by calling it on each thread sequentially, while the sleep() method can only pause the execution of the current thread.
The join() method is a blocking method, meaning it will block the calling thread until the target thread finishes, while the sleep() method is a non-blocking method, meaning it will not block the calling thread.
4. What is thread priority and how does it affect thread scheduling?
Thread priority is a hint (an int from Thread.MIN_PRIORITY 1 to MAX_PRIORITY 10, default NORM_PRIORITY 5) suggesting to the scheduler which threads are more important. Higher-priority threads may be favored for CPU time.
t.setPriority(Thread.MAX_PRIORITY); // 10 — just a hint
The crucial caveat interviewers want emphasized: priorities are only a hint and are largely unreliable. The JVM delegates scheduling to the OS, and behavior varies wildly across platforms — some operating systems ignore or compress Java priorities, others map them coarsely. So you must never rely on priority for correctness (ordering, mutual exclusion, or "this runs first"). Counting on it leads to non-portable, flaky behavior, and priorities can cause starvation of low-priority threads or, on some systems, priority inversion.
The modern framing: in practice you don't use thread priorities at all. For controlling execution you use proper concurrency tools — ExecutorService with appropriately sized pools, separate pools for different workloads, PriorityBlockingQueue for task ordering, and locks/coordination primitives for correctness. And with virtual threads (Java 21), priority is essentially irrelevant — they're scheduled cooperatively by the JVM. So the honest answer: priority exists, ranges 1–10, but it's a weak, platform-dependent hint you should not depend on; use higher-level concurrency constructs instead.
Follow-up 1
How can we set the priority of a thread in Java?
In Java, you can set the priority of a thread using the setPriority(int priority) method of the Thread class. The priority parameter should be an integer value ranging from 1 to 10. Here's an example of how to set the priority of a thread to the highest priority:
Thread thread = new Thread();
thread.setPriority(Thread.MAX_PRIORITY);
Follow-up 2
What is the default priority of a thread in Java?
In Java, the default priority of a thread is 5. This means that if you create a new thread without explicitly setting its priority, it will have a priority of 5 by default.
Follow-up 3
How does the JVM determine which threads to execute first based on their priorities?
The JVM uses a scheduling algorithm to determine the order in which threads are executed based on their priorities. The exact algorithm may vary across different JVM implementations. However, in general, higher priority threads are more likely to be scheduled for execution before lower priority threads. It is important to note that thread priority is just a hint to the thread scheduler and does not guarantee the exact order of execution. The thread scheduler is free to use different scheduling policies and may take other factors into consideration, such as the availability of CPU resources and the current state of the system.
5. What is thread synchronization in Java?
Thread synchronization is coordinating multiple threads so they access shared mutable state safely — ensuring that only one thread at a time enters a critical section, and that changes made by one thread are correctly visible to others. Without it, concurrent access causes race conditions, lost updates, and inconsistent state.
Two problems it solves: mutual exclusion (one thread in the critical section at a time) and visibility/ordering (a thread sees other threads' writes).
class Counter {
private int count;
synchronized void increment() { count++; } // only one thread at a time
}
The tools interviewers expect you to name:
synchronizedmethods/blocks (intrinsic monitor locks) andvolatile(visibility for a single variable).java.util.concurrenthigher-level constructs —ReentrantLock/ReadWriteLock, atomic classes (AtomicInteger), concurrent collections (ConcurrentHashMap), and coordinators (CountDownLatch,Semaphore).
The modern best-practice framing: prefer high-level concurrency utilities and immutability over hand-rolled synchronized — e.g. AtomicInteger for counters, ConcurrentHashMap instead of synchronizing a map, immutable objects to avoid shared mutable state entirely. And a current note: Java 21+ removed the "pinning" problem so that synchronized no longer blocks virtual threads' carrier threads — but the guidance to favor java.util.concurrent and immutability over manual locking still holds. The why is unchanged: prevent races and guarantee consistency.
Follow-up 1
Why is synchronization important in multithreading?
Synchronization is important in multithreading because it helps to prevent race conditions and ensure data consistency. When multiple threads access shared resources concurrently, there is a possibility of data corruption or inconsistent results. Synchronization allows threads to coordinate their access to shared resources, ensuring that only one thread can access the resource at a time.
Follow-up 2
What are the ways to achieve synchronization in Java?
There are two ways to achieve synchronization in Java:
Synchronized methods: By using the
synchronizedkeyword, you can declare a method as synchronized. This means that only one thread can execute the method at a time, preventing concurrent access to shared resources.Synchronized blocks: You can use synchronized blocks to create a synchronized section of code. By specifying an object as a lock, only one thread can enter the synchronized block at a time, ensuring mutual exclusion.
Follow-up 3
Can you explain the concept of a synchronized block in Java?
In Java, a synchronized block is a section of code that is synchronized on a specific object. It allows only one thread to enter the synchronized block at a time, ensuring mutual exclusion. The syntax for a synchronized block is as follows:
synchronized (object) {
// Code to be executed in a synchronized manner
}
The object specified in the synchronized block acts as a lock. Only one thread can acquire the lock at a time, preventing other threads from entering the synchronized block until the lock is released. This ensures that the code within the synchronized block is executed by only one thread at a time, providing synchronization.
6. What are virtual threads, and how do they differ from platform threads?
Virtual threads (Project Loom, final in Java 21) are lightweight threads managed by the JVM, not mapped 1:1 to OS threads. The JVM multiplexes many virtual threads onto a small pool of carrier (platform) threads: when a virtual thread blocks on I/O, it's unmounted so its carrier can run another — so blocking is cheap.
| Platform thread | Virtual thread | |
|---|---|---|
| Backed by | one OS thread | JVM-scheduled onto carriers |
| Cost / count | heavy (~1 MB stack); thousands | light; millions |
| Best for | CPU-bound | I/O-bound, high concurrency |
| Pooling | pool them (scarce) | don't pool — create one per task |
Thread.startVirtualThread(() -> handle(request));
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
requests.forEach(r -> exec.submit(() -> handle(r)));
}
Why it matters: virtual threads let you write simple, blocking, thread-per-request code that scales to enormous concurrency — without rewriting to async/reactive. Notes: they help I/O-bound, not CPU-bound, work; don't pool them (creation is cheap) and don't reuse via fixed pools; and since Java 21+, synchronized no longer "pins" a virtual thread to its carrier in most cases. This is one of the highest-signal modern Java topics in 2026 interviews.
Live mock interview
Mock interview: Basics of Multithreading
- Read your scene and goals
- Talk it out; goals tick off live
- Get a score and stronger lines
Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.