Insights into Company
← Java

Multithreading

Understand the idea. Compare it. Explain it. Test yourself.

Question 1 of 14

What is a process?

Solution and explanation

What it is

A process is a running instance of a program. When you start a Java application, the operating system creates a process for the JVM with its own:

  • memory address space (code, heap, stacks),
  • open files and network sockets,
  • environment, security context and process id (PID).

Processes are isolated from each other: one process cannot read another's memory directly. They communicate through inter-process communication (pipes, sockets, files, shared memory, message queues).

Example

Running the job portal API and a separate report generator gives two JVM processes:

$ jps
4120 PortalApplication      ← process 1 (its own heap)
4388 ReportGenerator        ← process 2 (its own heap)

Process vs thread (preview)

  • A process can contain many threads.
  • Threads inside one process share the same heap memory; processes do not share memory.
  • Creating a process is expensive (new address space); creating a thread is cheaper.
  • A crash in one process does not kill another; an uncaught error in a thread can corrupt shared state of its process.

Starting another process from Java

Process p = new ProcessBuilder("java", "-version").redirectErrorStream(true).start();
try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
    r.lines().forEach(System.out::println);
}
System.out.println("Exit code: " + p.waitFor());

Note: microservices run as separate processes (often in separate containers) so a failure or memory leak in one service does not take down the others.

Practice explaining this
Question 2 of 14

What is a thread?

Solution and explanation

What it is

A thread is a lightweight, independent path of execution inside a process. Every Java program starts with one thread – the main thread – and can create more. All threads of a process share the heap (objects, static fields) but each has its own stack (local variables, method calls) and program counter.

Process (JVM)
├── Heap (shared): objects, static data
├── Thread "main"        → own stack
├── Thread "worker-1"    → own stack
└── Thread "worker-2"    → own stack

Seeing threads

public class ThreadInfo {
    public static void main(String[] args) {
        Thread current = Thread.currentThread();
        System.out.println(current.getName() + " priority " + current.getPriority());

        Thread worker = new Thread(() ->
            System.out.println("Running in " + Thread.currentThread().getName()), "worker-1");
        worker.start();
    }
}
Output
main priority 5
Running in worker-1

Thread lifecycle (states)

  • NEW – created, start() not called yet.
  • RUNNABLE – running or ready to run.
  • BLOCKED – waiting to acquire a monitor lock.
  • WAITING / TIMED_WAITING – waiting for another thread or a timeout (join, wait, sleep).
  • TERMINATED – run() finished.
NEW --start()--> RUNNABLE --(lock busy)--> BLOCKED --(lock free)--> RUNNABLE
                     |--sleep()/wait()--> TIMED_WAITING / WAITING --> RUNNABLE
                     |--run() ends--> TERMINATED

Daemon threads

Background threads (garbage collector, monitoring) that do not keep the JVM alive. setDaemon(true) before start().

Virtual threads (Java 21)

Very cheap threads managed by the JVM rather than the OS, suitable for millions of blocking tasks (HTTP calls, database queries):

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) executor.submit(() -> callPaymentApi());
}

Note: call start(), never run() directly – run() executes on the current thread and creates no new thread.

Practice explaining this
Question 3 of 14

What is multithreading?

Solution and explanation

What it is

Multithreading in Java is a feature that lets a program run multiple threads at the same time, so tasks execute concurrently (and truly in parallel on multi-core CPUs) and the CPU is used more efficiently. A thread is a lightweight, independent unit of execution inside a process.

  • Threads allow parallel execution of tasks.
  • A process can have multiple threads.
  • Each thread runs independently but shares the same memory.

Example: imagine a restaurant kitchen. Several chefs (threads) prepare different dishes at the same time. Service is faster and every stove (CPU core) is in use.

          Main program (restaurant manager)
                       |
          starts multiple threads (chefs)
     ┌──────────┬──────────┬──────────┐
 Thread 1    Thread 2   Thread 3   Thread 4
 (Chef A)    (Chef B)   (Chef C)   (Chef D)
  run()       run()      run()      run()
     └──────────┴──── concurrent execution ┘

Way 1: extending the Thread class

Create a class that extends Thread and override run() with the task. Create objects and call start(), which creates a new thread and calls run() on it.

class CookingTask extends Thread {
    private final String task;

    CookingTask(String task) { this.task = task; }

    @Override
    public void run() {
        System.out.println(task + " is being prepared by " + Thread.currentThread().getName());
    }
}

public class Restaurant {
    public static void main(String[] args) {
        Thread t1 = new CookingTask("Pasta");
        Thread t2 = new CookingTask("Salad");
        Thread t3 = new CookingTask("Dessert");
        Thread t4 = new CookingTask("Rice");
        t1.start(); t2.start(); t3.start(); t4.start();
    }
}
Output (one possible order)
Rice is being prepared by Thread-3
Pasta is being prepared by Thread-0
Dessert is being prepared by Thread-2
Salad is being prepared by Thread-1

Explanation:

  • We created four threads (t1–t4) from the CookingTask class; each represents a dish.
  • start() creates a new thread with its own call stack and internally invokes run(), so the threads run concurrently with main and with each other.
  • Execution order is not fixed: after start(), the JVM and operating system decide when each thread runs, so output may differ on every run.

Way 2: implementing Runnable

Create a class that implements java.lang.Runnable, define run(), pass an instance to a Thread constructor and call start().

class CookingJob implements Runnable {
    private final String task;
    CookingJob(String task) { this.task = task; }

    @Override
    public void run() {
        System.out.println(task + " is being prepared by " + Thread.currentThread().getName());
    }
}

public class RestaurantRunnable {
    public static void main(String[] args) {
        Thread t1 = new Thread(new CookingJob("Soup"));
        Thread t2 = new Thread(new CookingJob("Pizza"));
        Thread t3 = new Thread(new CookingJob("Burger"));
        t1.start(); t2.start(); t3.start();
    }
}
Output (one possible order)
Burger is being prepared by Thread-2
Pizza is being prepared by Thread-1
Soup is being prepared by Thread-0

With Java 8 lambdas: new Thread(() -> System.out.println("Tea ready")).start();

Which way to use

  • extends Thread: only if your class does not extend any other class.
  • implements Runnable: preferred – your class can still extend another class (Java has no multiple inheritance), and the task is separated from the thread that runs it.
  • In real applications use an ExecutorService (thread pool) rather than creating threads by hand.

Waiting for threads to finish

t1.join();   // main waits until t1 completes

Advantages

  • Improved performance: multiple tasks run simultaneously.
  • Efficient CPU utilisation: cores stay busy.
  • Responsiveness: GUIs and servers stay responsive while background work runs.
  • Resource sharing: threads share memory, avoiding duplication.
  • Better user experience: downloads, animations and real-time updates run smoothly.

Challenges

  • Race conditions when threads modify shared data (use synchronization or atomic classes).
  • Deadlocks, starvation, harder debugging and testing.
  • Too many threads add context-switching overhead.

Note: a web server like Tomcat is multithreaded – each incoming HTTP request is handled by a thread from a pool.

Practice explaining this
Question 4 of 14

What is the difference between Thread and Runnable?

Solution and explanation

The difference

  • Thread is a class that represents a thread of execution (it also implements Runnable).
  • Runnable is a functional interface with one method, void run(), that represents a task.

So Runnable describes what to do; Thread is the worker that does it.

Side by side

// Extending Thread: task and thread are the same object
class EmailSender extends Thread {
    @Override public void run() { System.out.println("Sending email"); }
}
new EmailSender().start();

// Implementing Runnable: task is separate from the thread
class SmsSender implements Runnable {
    @Override public void run() { System.out.println("Sending SMS"); }
}
new Thread(new SmsSender()).start();

// Lambda Runnable
Runnable push = () -> System.out.println("Sending push notification");
new Thread(push).start();

Comparison

  • Inheritance: extending Thread uses up your single superclass; implementing Runnable leaves you free to extend another class.
  • Separation of concerns: Runnable separates the task from the execution mechanism, so the same task can run in a Thread, an ExecutorService or a scheduler.
  • Reuse: one Runnable instance can be submitted many times or shared by several threads.
  • Thread pools accept Runnable/Callable, not Thread subclasses.
  • Extending Thread is only worthwhile if you need to customise thread behaviour itself (rare).

Same task, many workers

Runnable counter = new Runnable() {
    private final AtomicInteger count = new AtomicInteger();
    public void run() { System.out.println(Thread.currentThread().getName() + " -> " + count.incrementAndGet()); }
};
new Thread(counter, "A").start();
new Thread(counter, "B").start();   // both threads share the same Runnable state

Limitation of Runnable

run() cannot return a value or throw checked exceptions. When you need a result, use Callable with an ExecutorService.

Note: interview answer – "prefer Runnable (or Callable) and run it with an ExecutorService".

Practice explaining this
Question 5 of 14

What is Callable?

Solution and explanation

What it is

Callable<V> is a functional interface like Runnable, but its call() method returns a result of type V and may throw checked exceptions. It is submitted to an ExecutorService, which returns a Future<V> holding the eventual result.

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Runnable vs Callable

  • Return value: run() returns void; call() returns V.
  • Exceptions: run() cannot throw checked exceptions; call() can.
  • Usage: Runnable with Thread or executor; Callable with ExecutorService.submit or invokeAll.

Example: fetch prices from three suppliers in parallel

ExecutorService pool = Executors.newFixedThreadPool(3);

Callable<Integer> supplierA = () -> { Thread.sleep(300); return 1200; };
Callable<Integer> supplierB = () -> { Thread.sleep(500); return 1150; };
Callable<Integer> supplierC = () -> { Thread.sleep(200); return 1300; };

List<Future<Integer>> quotes = pool.invokeAll(List.of(supplierA, supplierB, supplierC));
int best = Integer.MAX_VALUE;
for (Future<Integer> q : quotes) best = Math.min(best, q.get());   // get() waits for each result
System.out.println("Best price: " + best);
pool.shutdown();
Output (after about 500 ms instead of 1000 ms sequentially)
Best price: 1150

Exception handling

If call() throws, future.get() throws an ExecutionException wrapping the original cause:

try {
    future.get();
} catch (ExecutionException e) {
    System.out.println("Task failed: " + e.getCause().getMessage());
}

Note: for composing many asynchronous steps (call API → transform → save), CompletableFuture is more convenient than raw Callable/Future.

Practice explaining this
Question 6 of 14

What is Future?

Solution and explanation

What it is

A Future<V> represents the result of an asynchronous computation that may not have finished yet. You get one when you submit a Callable (or Runnable) to an ExecutorService.

Methods

  • get() – wait (block) until the result is ready, then return it.
  • get(timeout, unit) – wait at most the given time, else TimeoutException.
  • isDone() – has the task finished (normally, with an exception, or cancelled)?
  • cancel(mayInterruptIfRunning) – try to cancel the task.
  • isCancelled().

Example: generate a report without blocking the request thread

ExecutorService pool = Executors.newFixedThreadPool(4);

Future<byte[]> report = pool.submit(() -> buildSalaryReport());   // runs in the background

doOtherWork();                                                      // main thread keeps going

try {
    byte[] pdf = report.get(10, TimeUnit.SECONDS);                  // wait up to 10 s
    save(pdf);
} catch (TimeoutException e) {
    report.cancel(true);                                            // interrupt the worker
    System.out.println("Report took too long");
} catch (ExecutionException e) {
    System.out.println("Report failed: " + e.getCause());
}

Limitations of Future

  • get() blocks; there is no callback when the result arrives.
  • You cannot easily chain steps or combine several futures.
  • You cannot complete it manually.

CompletableFuture (Java 8+) solves these

CompletableFuture<Job> job = CompletableFuture.supplyAsync(() -> jobApi.fetch(5));
CompletableFuture<Company> company = CompletableFuture.supplyAsync(() -> companyApi.fetch(9));

job.thenCombine(company, (j, c) -> j.title() + " at " + c.name())
   .thenAccept(System.out::println)
   .exceptionally(ex -> { System.out.println("Failed: " + ex.getMessage()); return null; });

Note: always use a timeout with get() in servers; an unbounded wait can hang request threads.

Practice explaining this
Question 7 of 14

What is ExecutorService?

Solution and explanation

What it is

ExecutorService is a high-level API (java.util.concurrent) that manages a pool of worker threads. You submit tasks; the pool reuses its threads to run them, so you do not create and destroy a thread per task.

Why not new Thread() for every task?

  • Creating threads is expensive and unbounded creation can exhaust memory.
  • A pool limits concurrency (e.g. 10 parallel database calls) and queues extra tasks.
  • It provides results (Future), shutdown control and scheduling.

Creating pools

ExecutorService fixed = Executors.newFixedThreadPool(4);          // 4 threads, unbounded queue
ExecutorService cached = Executors.newCachedThreadPool();         // grows as needed (careful)
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();   // Java 21

Example: send 100 emails with 5 workers

ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 100; i++) {
    int id = i;
    pool.submit(() -> System.out.println("Email " + id + " sent by " + Thread.currentThread().getName()));
}
pool.shutdown();                                   // stop accepting new tasks
pool.awaitTermination(1, TimeUnit.MINUTES);       // wait for queued tasks
Output (excerpt)
Email 1 sent by pool-1-thread-1
Email 2 sent by pool-1-thread-2
Email 6 sent by pool-1-thread-1   ← threads are reused

Production configuration: ThreadPoolExecutor

ExecutorService pool = new ThreadPoolExecutor(
        5, 10,                                   // core and max threads
        60, TimeUnit.SECONDS,                    // idle time before extra threads die
        new ArrayBlockingQueue<>(500),           // bounded queue
        new ThreadPoolExecutor.CallerRunsPolicy()// back-pressure when full
);

Scheduling

scheduler.scheduleAtFixedRate(() -> refreshExchangeRates(), 0, 15, TimeUnit.MINUTES);

Shutdown

  • shutdown(): finish queued tasks, accept no new ones.
  • shutdownNow(): interrupt running tasks and return the queued ones.

Always shut pools down, otherwise their non-daemon threads keep the JVM alive.

Note: in Spring, configure a ThreadPoolTaskExecutor bean and use @Async instead of creating pools manually.

Practice explaining this
Question 8 of 14

What is synchronization?

Solution and explanation

What it is

Synchronization controls access to shared data so that only one thread at a time executes a critical section. It prevents race conditions and also guarantees visibility: changes made inside a synchronized block are visible to the next thread that acquires the same lock.

Every Java object has an intrinsic lock (monitor). A thread must acquire it to enter a synchronized method or block on that object.

Without synchronization: lost updates

class Ticketing {
    private int sold = 0;
    void sell() { sold++; }          // read, add, write – not atomic
    int sold() { return sold; }
}

Two threads each selling 10,000 tickets often print fewer than 20,000.

Synchronized method

class Ticketing {
    private int sold = 0;
    synchronized void sell() { sold++; }      // lock on "this"
    synchronized int sold() { return sold; }
}

Synchronized block (lock only what is needed)

class BankAccount {
    private final Object lock = new Object();
    private long balance;

    void withdraw(long amount) {
        validate(amount);                     // no lock needed
        synchronized (lock) {                 // critical section
            if (balance < amount) throw new IllegalStateException("Insufficient balance");
            balance -= amount;
        }
    }
}

Static synchronized

static synchronized methods lock the Class object, protecting static data.

Demonstration

Ticketing t = new Ticketing();
Thread a = new Thread(() -> { for (int i = 0; i < 10_000; i++) t.sell(); });
Thread b = new Thread(() -> { for (int i = 0; i < 10_000; i++) t.sell(); });
a.start(); b.start(); a.join(); b.join();
System.out.println(t.sold());   // always 20000 with synchronized

Alternatives

  • AtomicInteger/AtomicLong for single counters (lock-free).
  • ReentrantLock: tryLock with timeout, fair locks, multiple conditions.
  • Concurrent collections (ConcurrentHashMap, BlockingQueue).
  • Immutable objects need no locking at all.

Costs

Locks serialise threads (less parallelism) and can cause deadlock if acquired in inconsistent order. Keep critical sections small.

Practice explaining this
Question 9 of 14

What is a race condition?

Solution and explanation

What it is

A race condition occurs when the result of a program depends on the unpredictable timing of threads accessing shared data, and at least one thread writes. The classic case is a read-modify-write that is not atomic.

Example: two withdrawals from one account

Balance = ₹1,000. Two ATM threads withdraw ₹800 at the same moment:

Thread A: read balance = 1000
Thread B: read balance = 1000
Thread A: 1000 >= 800 → write 200
Thread B: 1000 >= 800 → write 200
Result: ₹1,600 paid out, balance 200   ← money lost
class Account {
    private int balance = 1000;
    void withdraw(int amount) {
        if (balance >= amount) {          // check
            sleepBriefly();               // widens the window
            balance -= amount;            // act
        }
    }
}

Another example: count++

count++ is three steps (read, increment, write). Two threads incrementing 1,000 times each can end with less than 2,000.

Fixes

  1. synchronized or a Lock around the check-and-act:
synchronized void withdraw(int amount) {
    if (balance >= amount) balance -= amount;
}
  1. Atomic classes:
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
balance.updateAndGet(b -> b >= amount ? b - amount : b);   // atomic compare-and-set loop
  1. Thread-confinement or immutability – do not share mutable state.
  2. In databases: transactions with row locks (SELECT ... FOR UPDATE) or optimistic locking (@Version).

Detecting race conditions

They are intermittent and may disappear under a debugger. Use stress tests, tools such as jcstress, and code review of shared mutable state.

Note: volatile does not fix check-then-act races; it only guarantees visibility, not atomicity.

Practice explaining this
Question 10 of 14

What is deadlock?

Solution and explanation

What it is

A deadlock is a situation where two or more threads wait forever, each holding a lock the other needs. None can proceed.

Example: two transfers in opposite directions

class Account { final Object lock = new Object(); long balance; }

void transfer(Account from, Account to, long amount) {
    synchronized (from.lock) {
        synchronized (to.lock) {
            from.balance -= amount;
            to.balance += amount;
        }
    }
}
// Thread 1: transfer(A, B) locks A, waits for B
// Thread 2: transfer(B, A) locks B, waits for A  → deadlock
Thread 1 holds A ──waits for──► B
Thread 2 holds B ──waits for──► A

Four conditions (all required)

  1. Mutual exclusion – a resource is held by one thread.
  2. Hold and wait – a thread holds one lock while waiting for another.
  3. No preemption – locks cannot be taken away.
  4. Circular wait – a cycle of threads waiting on each other.

Break any one to prevent deadlock.

Prevention

  1. Consistent lock ordering (break circular wait):
void transfer(Account from, Account to, long amount) {
    Account first = from.id < to.id ? from : to;
    Account second = first == from ? to : from;
    synchronized (first.lock) {
        synchronized (second.lock) { /* move money */ }
    }
}
  1. Timeouts with ReentrantLock.tryLock – give up and retry instead of waiting forever.
  2. Hold fewer locks and for less time; avoid calling external code while holding a lock.

Detecting deadlocks

  • jstack <pid> or a thread dump prints "Found one Java-level deadlock" with the threads and locks involved.
  • ThreadMXBean.findDeadlockedThreads() programmatically.

Note: databases detect deadlocks themselves (MySQL error 1213) and roll back one transaction; your code should retry it.

Practice explaining this

Showing questions 1–10 of 14